Fortgeschrittenes TypeScript mit einem echten Beispiel (Teil 1)
Veröffentlicht: 2020-11-06Letzte Woche haben wir eine kleine Einführung in TypeScript gesehen und insbesondere darüber gesprochen, wie diese Sprache, die JavaScript erweitert, uns helfen kann, robusteren Code zu erstellen. Da dies nur eine Einführung war, habe ich nicht über einige der TypeScript-Funktionen gesprochen, die Sie möglicherweise in Ihren Projekten verwenden möchten (und wahrscheinlich benötigen).
Heute zeige ich Ihnen, wie Sie TypeScript professionell in einem realen Projekt anwenden. Dazu schauen wir uns zunächst einen Teil des Quellcodes von Nelio Content an, um zu verstehen, wo wir anfangen und welche Einschränkungen wir derzeit haben. Als Nächstes werden wir den ursprünglichen JavaScript-Code schrittweise verbessern, indem wir kleine inkrementelle Verbesserungen hinzufügen, bis wir einen vollständig typisierten Code haben.
Verwendung des Quellcodes von Nelio Content als Grundlage
Wie Sie vielleicht bereits wissen, ist Nelio Content ein Plugin, mit dem Sie den Inhalt Ihrer Website in sozialen Medien teilen können. Darüber hinaus enthält es auch mehrere Funktionen, die Ihnen helfen sollen, ständig bessere Inhalte in Ihrem Blog zu erstellen, wie z. B. eine Qualitätsanalyse Ihrer Beiträge, einen Redaktionskalender, um den Überblick über die anstehenden Inhalte zu behalten, die Sie schreiben müssen, und so weiter .

Letzten Monat haben wir Version 2.0 veröffentlicht, eine komplette Neugestaltung sowohl visuell als auch intern unseres Plugins. Wir haben diese Version mit allen neuen Technologien erstellt, die uns heute in WordPress zur Verfügung stehen (etwas, worüber wir kürzlich in unserem Blog gesprochen haben), einschließlich einer React-Oberfläche und eines Redux-Speichers.
Im heutigen Beispiel werden wir also letzteres verbessern. Das heißt, wir werden sehen, wie wir einen Redux-Speicher eingeben können.
Nelio Content Editorial Kalender-Selektoren
Der Redaktionskalender ist eine Benutzeroberfläche, die die Blogbeiträge anzeigt, die wir für jeden Wochentag geplant haben. Das bedeutet, dass unser Redux-Speicher mindestens zwei Abfrageoperationen benötigt: eine, die uns die Posts mitteilt, die an einem bestimmten Tag geplant sind, und eine andere, die bei gegebener Post-ID alle ihre Attribute zurückgibt.
Vorausgesetzt, Sie haben unsere Beiträge zu diesem Thema gelesen, wissen Sie bereits, dass ein Selektor in Redux als ersten Parameter den Status mit allen Informationen unserer App erhält, gefolgt von allen zusätzlichen Parametern, die er möglicherweise benötigt. Unsere beiden Beispielselektoren in JavaScript würden also etwa so aussehen:
function getPost( state, id ) { return state.posts[ id ]; } function getPostsInDay( state, day ) { return state.days[ day ] ?? []; } Wenn Sie sich fragen, woher ich weiß, dass ein Zustand die Attribute posts und days hat, ist es ziemlich einfach: weil ich derjenige bin, der sie definiert hat. Aber hier ist der Grund, warum ich mich entschieden habe, sie so zu implementieren.
Wir wissen, dass wir aus zwei verschiedenen Blickwinkeln auf unsere Informationen zugreifen möchten: Posts an einem Tag oder Posts nach ID. Es scheint also sinnvoll, unsere Daten in zwei Teile zu gliedern:
- Zum einen haben wir ein
posts-Attribut, in dem wir alle Posts aufgelistet haben, die wir vom Server bezogen und in unserem Redux-Store gespeichert haben. Logischerweise hätten wir sie in einem Array speichern und eine sequentielle Suche durchführen können, um den Beitrag zu finden, dessen ID mit der erwarteten übereinstimmt … aber ein Objekt verhält sich wie ein Wörterbuch und bietet schnellere Suchvorgänge. - Andererseits müssen wir auch auf Posts zugreifen, die für einen bestimmten Tag geplant sind. Auch hier hätten wir nur ein einziges Array verwenden können, um alle Posts zu speichern und es zu filtern, um die Posts zu finden, die zu einem bestimmten Tag gehören, aber ein weiteres Wörterbuch bietet eine schnellere Suchlösung.
Aktionen und Reduzierungen in Nelio-Inhalten
Wenn wir schließlich einen dynamischen Kalender wollen, müssen wir Funktionen implementieren, die es uns ermöglichen, die Informationen zu aktualisieren, die unser Geschäft speichert. Der Einfachheit halber schlagen wir zwei einfache Methoden vor: eine, mit der wir neue Beiträge zum Kalender hinzufügen können, und eine andere, mit der wir die Attribute der vorhandenen ändern können.
Updates für einen Redux-Speicher erfordern zwei Teile. Auf der einen Seite haben wir Aktionen, die die Änderung signalisieren, die wir vornehmen möchten, und auf der anderen Seite gibt es einen Reducer, der angesichts des aktuellen Zustands unseres Shops und einer Aktion, die eine Aktualisierung anfordert, die notwendigen Änderungen auf den aktuellen Zustand anwendet einen neuen Zustand erzeugen.
In Anbetracht dessen sind dies die Aktionen, die wir möglicherweise in unserem Geschäft haben:
function receiveNewPost( post ) { return { type: 'RECEIVE_NEW_POST', post, }; } function updatePost( postId, attributes ) { return { type: 'UPDATE_POST', postId, attributes, } }und hier der minderer:
function reducer( state, action ) { state = state ?? { posts: {}, days: {} }; const postIds = Object.keys( state.posts ); switch ( action.type ) { case 'RECEIVE_NEW_POST'; if ( postIds.includes( action.postId ) ) { return state; } return { posts: { ...state.posts, [ action.post.id ]: action.post, }, days: { ...state.days, [ action.post.day ]: [ ...state.days[ action.post.day ], action.post.id, ], }, }; case 'UPDATE_POST'; if ( ! postIds.includes( action.postId ) ) { return state; } const post = { ...state.posts[ action.postId ], ...action.attributes, }; return { posts: { ...state.posts, [ post.id ]: post, }, days: { ...Object.keys( state.days ).reduce( ( acc, day ) => ( { ...acc, [ day ]: state.days[ day ].filter( ( postId ) => postId !== post.id ), } ), {} ), [ post.day ]: [ ...state.days[ post.day ], post.id, ], }, }; } return state; }Nehmen Sie sich Zeit, um alles zu verstehen, und lassen Sie uns weitermachen!
Von JavaScript zu TypeScript
Als erstes sollten wir den vorherigen Code in TypeScript übersetzen. Nun, da TypeScript eine Obermenge von JavaScript ist, ist es das bereits … aber wenn Sie die vorherigen Funktionen kopieren und in den TypeScript Playground einfügen, werden Sie sehen, dass sich der Compiler ziemlich beschwert, weil es zu viele Variablen gibt, deren impliziter Typ any ist. Lassen Sie uns das zuerst beheben, indem wir explizit einige grundlegende Typen hinzufügen.
Alles, was wir tun müssen, ist, den any Typ explizit zu allem hinzuzufügen, was „komplex“ ist (z. B. dem Status unserer Anwendung), und eine number oder einen string oder was auch immer wir für eine andere Variable/ein anderes Argument verwenden möchten. Zum Beispiel der ursprüngliche JavaScript-Selektor:
function getPost( state, id ) { return state.posts[ id ]; }mit expliziten TypeScript-Typen würde so aussehen:
function getPost( state: any, id: number ): any | undefined { return state.posts[ id ]; } Wie Sie sehen können, bietet die einfache Eingabe unseres Codes (selbst wenn wir „generische Typen“ verwenden) viele Informationen auf einen Blick; eine deutliche Verbesserung gegenüber einfachem JavaScript! In diesem Fall sehen wir zum Beispiel, dass getPost eine number erwartet (eine Beitrags-ID ist eine Ganzzahl, erinnerst du dich?) und das Ergebnis entweder etwas ist, wenn der Beitrag existiert ( any ), oder nichts, wenn er nicht existiert ( undefined ).
Hier haben Sie den Link mit allen Codetypen, die einfache Typen verwenden, damit sich der Compiler nicht beschwert.
Erstellen und verwenden Sie benutzerdefinierte Datentypen in TypeScript
Jetzt, da der Compiler mit unserem Quellcode zufrieden ist, ist es an der Zeit, ein wenig darüber nachzudenken, wie wir ihn verbessern können. Dafür schlage ich immer vor, mit der Modellierung der Konzepte zu beginnen, die wir in unserem Bereich haben.

Erstellen eines benutzerdefinierten Typs für Beiträge
Wir wissen, dass unser Shop in erster Linie Posts enthalten wird, daher würde ich sagen, dass der erste Schritt darin besteht, zu modellieren, was ein Post ist und welche Informationen wir darüber haben. Wir haben bereits letzte Woche gesehen, wie man benutzerdefinierte Typen erstellt, also versuchen wir es heute mit dem Post-Konzept:
type Post = { id: number; title: string; author: string; day: string; status: string; isSticky: boolean; }; Hier gibt es keine Überraschungen, oder? Ein Post ist ein Objekt mit einigen Attributen, wie z. B. einer numerischen id , einem title und so weiter.
Eine weitere wichtige Information, über die jeder Redux-Speicher verfügt, ist, Sie haben es erraten, sein Zustand. Im vorherigen Abschnitt haben wir bereits die Attribute besprochen, die er hat, also definieren wir die Grundform unseres State Typs:
type State = { posts: any; days: any; }; Verbesserung des State
Jetzt wissen wir, dass State zwei Attribute hat ( posts und days ), aber wir wissen any viel darüber, was sie sind, da sie alles Mögliche sein können. Wir sagten, wir wollten, dass beide Attribute Wörterbücher sind. Das heißt, bei einer bestimmten Abfrage (entweder eine Beitrags-ID für posts oder ein Datum für days ) möchten wir die zugehörigen Daten (einen Beitrag bzw. eine Liste von Beiträgen). Wir wissen, dass wir ein Wörterbuch mit einem Objekt implementieren können, aber wie stellen wir ein Wörterbuch in TypeScript dar?
Wenn wir uns die TypeScript-Dokumentation ansehen, werden wir sehen, dass sie mehrere Hilfstypen enthält, um mit ziemlich häufigen Situationen fertig zu werden. Insbesondere gibt es einen Typ namens Record , der anscheinend der ist, den wir wollen: Er ermöglicht es uns, eine Variable mit Schlüssel/Wert-Paaren einzugeben, in denen der Schlüssel einen bestimmten Keys -Typ hat und die Werte vom Typ Type sind. Wenn wir diesen Typ auf unser Beispiel anwenden, würden wir so etwas erhalten:
type State = { posts: Record<number, Post>; days: Record<string, number[]>; }; Aus Sicht des Compilers funktioniert der Record -Typ so, dass sein Ergebnis bei jedem Wert von Keys (in unserem Beispiel number für posts und string für days ) immer ein Objekt vom Typ Type ist (in unserem Fall a Post bzw. eine number[] ). Das Problem ist, dass sich unser Wörterbuch nicht so verhalten soll: Wenn wir anhand seiner ID nach einem bestimmten Beitrag suchen, möchten wir, dass der Compiler weiß, ob wir einen verwandten Beitrag finden oder nicht, was bedeutet, dass das Ergebnis entweder ein Post sein kann oder undefined .
Glücklicherweise können wir dies leicht beheben, indem wir einen weiteren Hilfstyp verwenden, den Partial -Typ:
type State = { posts: Partial< Record<number, Post> >; days: Partial< Record<string, number[]> >; };Verbesserung unseres Codes mit Type Aliases
Werfen Sie einen Blick auf das posts -Attribut in unserem Bundesstaat… Was sehen Sie? Ein Wörterbuch, das Beiträge vom Typ Post mit Nummern indiziert, richtig? Stellen Sie sich jetzt vor, wie Sie diesen Code bei der Arbeit überprüfen. Wenn Sie auf einen solchen Typ stoßen, könnten Sie davon ausgehen, dass eine number , die Beiträge indiziert, wahrscheinlich die ID der indizierten Beiträge ist … aber das ist nur eine Annahme; Sie müssten den Code überprüfen, um sicher zu sein. Und was ist mit days ? „Zufällige Zeichenfolgen, die Listen von Zahlen indizieren.“ Das ist nicht sehr hilfreich, oder?
TypeScript-Typen helfen uns dank der Compiler-Prüfungen, robusteren Code zu schreiben, aber sie bieten weit mehr als das. Wenn Sie aussagekräftige Typen verwenden, wird Ihr Code besser dokumentiert und einfacher zu warten. Lassen Sie uns also vorhandene Typen aliasieren, um aussagekräftige Typen zu erstellen, sollen wir?
Da wir beispielsweise wissen, dass Post-IDs ( number ) und Datumsangaben ( string ) für unsere Domain relevant sind, können wir einfach die folgenden Typ-Aliase erstellen:
type PostId = number; type Day = string;und schreiben Sie dann unsere ursprünglichen Typen mit diesen Aliasnamen um:
type Post = { id: PostId; title: string; author: string; day: Day; status: string; isSticky: boolean; }; type State = { posts: Partial< Record<PostId, Post> >; days: Partial< Record<Day, PostId[]> >; }; Ein weiterer Typ-Alias, den wir verwenden können, um die Lesbarkeit unseres Codes zu verbessern, ist der Dictionary -Typ, der die Komplexität der Verwendung von Partial und Record hinter einer praktischen Struktur „versteckt“:
type Dictionary<K extends string | number, T> = Partial< Record<K, T> >;Unseren Quellcode klarer machen:
type State = { posts: Dictionary<PostId, Post>; days: Dictionary<Day, PostId[]>; }; Und das ist es! Hier hast du es! Mit nur drei einfachen Typaliasen konnten wir den Code auf eine Weise dokumentieren, die deutlich besser ist als die Verwendung von Kommentaren. Jeder Entwickler, der nach uns kommt, wird auf einen Blick erkennen können, dass posts ein Wörterbuch ist, das Objekte des Typs Post anhand ihrer PostId , und dass days eine Datenstruktur ist, die bei einem Day eine Liste von Post-IDs zurückgibt. Das ist ziemlich genial, wenn du mich fragst.
Aber nicht nur Typdefinitionen selbst sind besser … wenn wir diese neuen Typen in unserem gesamten Code verwenden:
function getPost( state: State, id: PostId ): Post | undefined { return state.posts[ id ]; }es profitiert auch von dieser neuen semantischen Schicht! Sie können die neue Version unseres getippten Codes hier sehen.
Übrigens, denken Sie daran, dass Typaliase aus der Sicht des Compilers nicht vom „ursprünglichen“ Typ zu unterscheiden sind. Das bedeutet, dass zum Beispiel eine PostId und eine number vollständig austauschbar sind. Erwarten Sie also nicht, dass der Compiler einen Fehler auslöst, wenn Sie einer number eine PostId zuweisen oder umgekehrt (wie Sie in diesem kleinen Beispiel sehen können); Sie dienen lediglich dazu, unserem Quellcode Semantik hinzuzufügen.
Nächste Schritte
Wie Sie sehen können, können Sie JavaScript-Code mithilfe von TypeScript-Typen inkrementell eingeben, wodurch sich seine Qualität und Lesbarkeit verbessert. Im heutigen Beitrag haben wir ein Beispiel für eine reale Implementierung einer React + Redux-Anwendung ausführlich gesehen und gesehen, wie sie mit relativ geringem Aufwand verbessert werden könnte. Aber wir haben noch einen langen Weg vor uns.
Im nächsten Beitrag werden wir alle verbleibenden Variablen/Argumente eingeben, die derzeit den any Typ verwenden, und wir werden auch einige fortgeschrittene TypeScript-Leistungen lernen. Ich hoffe, Ihnen hat dieser erste Teil gefallen und wenn ja, teilen Sie ihn bitte mit Ihren Freunden und Kollegen.
Vorgestelltes Bild von Danielle MacInnes auf Unsplash.
