Hallo Bewohner! Wir haben der Druckerei eine weitere Neuheit
" Professional TypeScript. Entwicklung skalierbarer JavaScript-Anwendungen " vorgelegt . In diesem Buch lernen Programmierer, die bereits mit JavaScript vertraut sind, wie man TypeScript beherrscht. Sie werden sehen, wie TypeScript Ihnen helfen kann, Ihren Code bis zu 10x besser zu skalieren und die Programmierung wieder unterhaltsam zu gestalten.
Unten finden Sie einen Auszug aus einem Kapitel aus dem Buch "Advanced Types".
Erweiterte Typen
Das weltbekannte TypeScript-Typensystem überrascht selbst Haskell-Programmierer mit seinen Funktionen. Wie Sie bereits wissen, ist es nicht nur ausdrucksstark, sondern auch einfach zu verwenden: Typeinschränkungen und -beziehungen sind präzise, verständlich und werden in den meisten Fällen automatisch abgeleitet.
Das Modellieren dynamischer JavaScript-Elemente wie Prototypen, die daran gebunden sind, Funktionsüberladungen und sich ständig ändernde Objekte erfordert ein Typsystem und deren Operatoren, die so umfangreich sind, wie Batman es tun würde.
Ich beginne dieses Kapitel mit einem tiefen Einblick in die Themen Subtypisierung, Kompatibilität, Varianz, Zufallsvariable und Erweiterung. Dann werde ich auf die Besonderheiten der flussbasierten Typprüfung eingehen, einschließlich Verfeinerung und Gesamtheit. Als Nächstes werde ich einige erweiterte Programmierfunktionen auf Typebene demonstrieren: Verbinden und Zuordnen von Objekttypen, Verwenden von bedingten Typen, Definieren von Typschutzfunktionen und Fallback-Lösungen wie Typzusicherungen und explizite Zuweisungszusicherungen. Abschließend möchte ich Ihnen einige erweiterte Muster zur Verbesserung der Typensicherheit vorstellen: Musterung von Begleitobjekten, Schnittstellenverbesserungen für Tupel, Nachahmung von Nominaltypen und sichere Prototyperweiterung.
Beziehungen zwischen Typen
Schauen wir uns die Beziehungen in TypeScript genauer an.
Subtypen und Supertypen
Wir haben bereits im Abschnitt "Über Typen" auf Seite 22 die Kompatibilität angesprochen. 34, also lassen Sie uns gleich mit diesem Thema beginnen und mit der Definition des Subtyps beginnen.

Zurück zu Abb. 3.1 und siehe TypeScript's integrierte Subtyp-Zuordnungen.

- Array ist ein Subtyp eines Objekts.
- Ein Tupel ist ein Subtyp eines Arrays.
- Alles ist ein Subtyp von jedem.
- Niemals ist ein Subtyp von allem.
- Die Bird-Klasse, die die Animal-Klasse erweitert, ist ein Subtyp der Animal-Klasse.
Nach der Definition, die ich gerade für einen Subtyp gegeben habe, bedeutet dies:
- Wo immer Sie ein Objekt benötigen, können Sie ein Array verwenden.
- Wo immer ein Array benötigt wird, kann ein Tupel verwendet werden.
- Wo immer Sie welche benötigen, können Sie ein Objekt verwenden.
- Kann niemals überall verwendet werden.
- Wo immer Sie ein Tier brauchen, können Sie Bird verwenden.
Ein Supertyp ist das Gegenteil eines Subtyps.
SUPERTYP
Wenn Sie zwei Typen haben, A und B, und B ein Supertyp von A ist, können Sie A überall dort sicher verwenden, wo B benötigt wird (Abbildung 6.2).

Und wieder, basierend auf dem Diagramm in Abb. 3.1:
- Array ist ein Supertyp von Tupel.
- Objekt ist ein Supertyp eines Arrays.
- Any ist ein Supertyp von allem.
- Niemals ist niemandes Supertyp.
- Tier ist ein Supertyp von Vogel.
Es ist genau das Gegenteil von Subtypen und nichts weiter.
Variation
Für die meisten Typen ist es leicht zu verstehen, ob ein bestimmter Typ A ein Subtyp von B ist. Für einfache Typen wie Zahl, Zeichenfolge usw. können Sie sich auf das Diagramm in Abb. 1 beziehen. 3.1 oder unabhängig bestimmen, dass die in der Gewerkschaftsnummer enthaltene Nummer | Zeichenfolge ist ein Untertyp dieser Vereinigung.
Es gibt jedoch komplexere Typen wie Generika. Betrachten Sie diese Fragen:
- Wann ist Array <A> ein Subtyp von Array <B>?
- Wann ist Form A ein Subtyp von Form B?
- Wann ist Funktion (a: A) => B ein Subtyp der Funktion (c: C) => D?
Subtypisierungsregeln für Typen, die andere Typen enthalten (dh Typparameter wie Array <A>, Formulare mit Feldern wie {a: number} oder Funktionen wie (a: A) => B), sind schwieriger zu verstehen, da dies nicht der Fall ist konsistent über verschiedene Programmiersprachen.
Um das Lesen der folgenden Regeln zu vereinfachen, werde ich einige Syntaxelemente vorstellen, die in TypeScript nicht funktionieren (keine Sorge, es ist nicht mathematisch):
- A <: B bedeutet "A ist ein Subtyp des gleichen Typs wie B";
- A>: B bedeutet "A ist ein Supertyp des gleichen Typs wie Typ B".
Variation von Form und Array
Um zu verstehen, warum Sprachen in den Regeln für die Subtypisierung komplexer Typen nicht übereinstimmen, hilft ein Beispiel mit einem Formular, das einen Benutzer in einer Anwendung beschreibt. Wir repräsentieren es durch einige Arten:
// , .
type ExistingUser = {
id: number
name: string
}
// , .
type NewUser = {
name: string
}
Angenommen, ein Praktikant in Ihrem Unternehmen muss Code schreiben, um einen Benutzer zu löschen. Er beginnt mit folgendem:
function deleteUser(user: {id?: number, name: string}) {
delete user.id
}
let existingUser: ExistingUser = {
id: 123456,
name: 'Ima User'
}
deleteUser(existingUser)
deleteUser empfängt ein Objekt vom Typ {id ?: number, name: string} und übergibt ihm einen vorhandenen Benutzer vom Typ {id: number, name: string}. Beachten Sie, dass der Typ der Eigenschafts-ID (Nummer) ein Subtyp des erwarteten Typs (Nummer | undefiniert) ist. Daher ist das gesamte Objekt {id: number, name: string} ein Subtyp von {id ?: Number, name: string}, sodass TypeScript dies zulässt.
Sehen Sie Sicherheitsprobleme? Es gibt eine Möglichkeit: Nach der Übergabe von ExistingUser an deleteUser weiß TypeScript nicht, dass die Benutzer-ID gelöscht wurde. Wenn Sie also nach dem Löschen von existUser.id die vorhandene Benutzer-ID lesen, geht TypeScript weiterhin davon aus, dass die vorhandene Benutzer-ID die Typennummer hat.
Offensichtlich ist die Verwendung eines Objekttyps, bei dem sein Supertyp erwartet wird, unsicher. Warum erlaubt TypeScript dies? Das Fazit ist, dass es nicht völlig sicher sein sollte. Sein Typensystem versucht, echte Fehler zu erkennen und sie für Programmierer aller Ebenen sichtbar zu machen. Da destruktive Aktualisierungen (wie das Löschen einer Eigenschaft) in der Praxis relativ selten sind, wird TypeScript gelockert und ermöglicht es Ihnen, ein Objekt zuzuweisen, dessen Supertyp erwartet wird.
Was ist mit dem umgekehrten Fall: Ist es möglich, ein Objekt zuzuweisen, dessen Subtyp erwartet wird?
Fügen wir einen neuen Typ für den alten Benutzer hinzu und entfernen Sie dann den Benutzer mit diesem Typ (stellen Sie sich vor, Sie fügen dem Code, den Ihr Kollege geschrieben hat, Typen hinzu):
type LegacyUser = {
id?: number | string
name: string
}
let legacyUser: LegacyUser = {
id: '793331',
name: 'Xin Yang'
}
deleteUser(legacyUser) // TS2345: a 'LegacyUser'
//
// '{id?: number |undefined, name: string}'.
// 'string' 'number |
// undefined'.
Wenn Sie ein Formular mit einer Eigenschaft senden, deren Typ ein Supertyp des erwarteten Typs ist, schwört TypeScript. Dies liegt daran, dass id eine Zeichenfolge | ist Nummer | undefined und deleteUser behandelt nur den Fall, in dem id number | ist nicht definiert.
Während Sie ein Formular erwarten, können Sie einen Typ mit Eigenschaftstypen übergeben, die <: der erwarteten Typen sind. Sie können jedoch kein Formular ohne Eigenschaftstypen übergeben, die Supertypen der erwarteten Typen sind. Wenn wir über Typen sprechen, sagen wir: "TypeScript-Formulare (Objekte und Klassen) sind in den Typen ihrer Eigenschaften kovariant." Das heißt, damit Objekt A Objekt B zugewiesen werden kann, muss jede seiner Eigenschaften <: die entsprechende Eigenschaft in B sein.
Kovarianz ist eine von vier Arten von Varianz:
Invarianz
Speziell benötigte T.
Kovarianz
Benötigt <: T.
Erforderliche Kontravarianz
>: T.
Bivarianz Passt
entweder zu <: T oder>: T.
In TypeScript ist jeder komplexe Typ in seinen Elementen kovariant - Objekte, Klassen, Arrays und Funktionsrückgabetypen - mit einer Ausnahme: Die Typen der Funktionsparameter sind kontravariant.
. , . ( ). , Scala, Kotlin Flow, , .
TypeScript : , , , (, id deleteUser, , , ).
Variation der Funktion
Betrachten wir einige Beispiele.
Funktion A ist ein Subtyp von Funktion B, wenn A die gleiche oder eine geringere Arität (Anzahl der Parameter) als B hat, und:
- Der Typ this, der zu A gehört, ist entweder undefiniert oder>: des Typs this, der zu B gehört.
- Jeder der Parameter A>: der entsprechende Parameter in B.
- Rückgabetyp A <: Rückgabetyp B.
Beachten Sie, dass, damit Funktion A ein Subtyp von Funktion B ist, dieser Typ und diese Parameter>: Gegenstücke in B sein müssen, während ihr Rückgabetyp <: sein muss. Warum kehrt sich der Zustand um? Warum funktioniert die einfache <: Bedingung nicht für jede Komponente (Typ this, Parametertypen und Rückgabetyp), wie dies bei Objekten, Arrays, Gewerkschaften usw. der Fall ist?
Beginnen wir mit der Definition von drei Typen (anstelle der Klasse können Sie auch andere Typen verwenden, wobei A: <B <: C):
class Animal {}
class Bird extends Animal {
chirp() {}
}
class Crow extends Bird {
caw() {}
}
Also Crow <: Bird <: Animal.
Definieren wir eine Funktion, die Bird nimmt und zum Twittern bringt:
function chirp(bird: Bird): Bird {
bird.chirp()
return bird
}
So weit, ist es gut. Was erlaubt Ihnen TypeScript, um zu zwitschern?
chirp(new Animal) // TS2345: 'Animal'
chirp(new Bird) // 'Bird'.
chirp(new Crow)
Eine Bird-Instanz (als Chirp-Parameter vom Typ Bird) oder eine Crow-Instanz (als Subtyp von Bird). Die Übergabe des Subtyps funktioniert wie erwartet.
Lassen Sie uns eine neue Funktion erstellen. Diesmal ist sein Parameter eine Funktion:
function clone(f: (b: Bird) => Bird): void {
// ...
}
Der Klon benötigt eine Funktion f, die Bird empfängt und Bird zurückgibt. Welche Arten von Funktionen können sicher an f übergeben werden? Offensichtlich ist die Funktion, die Bird empfängt und zurückgibt:
function birdToBird(b: Bird): Bird {
// ...
}
clone(birdToBird) // OK
Was ist mit einer Funktion, die einen Vogel nimmt, aber eine Krähe oder ein Tier zurückgibt?
function birdToCrow(d: Bird): Crow {
// ...
}
clone(birdToCrow) // OK
function birdToAnimal(d: Bird): Animal {
// ...
}
clone(birdToAnimal) // TS2345: '(d: Bird) =>
// Animal'
// '(b: Bird) => Bird'. 'Animal'
// 'Bird'.
birdToCrow funktioniert wie erwartet, aber birdToAnimal gibt einen Fehler aus. Warum? Stellen Sie sich vor, eine Klonimplementierung sieht folgendermaßen aus:
function clone(f: (b: Bird) => Bird): void {
let parent = new Bird
let babyBird = f(parent)
babyBird.chirp()
}
Indem wir die Funktion f an clone übergeben, die Animal zurückgibt, können wir darin .chirp nicht aufrufen. Daher muss TypeScript sicherstellen, dass die Funktion, die wir übergeben, mindestens Bird zurückgibt.
Wenn wir sagen, dass Funktionen in ihren Rückgabetypen kovariant sind, bedeutet dies, dass eine Funktion nur dann ein Subtyp einer anderen Funktion sein kann, wenn ihr Rückgabetyp <: der Rückgabetyp dieser Funktion ist.
Okay, was ist mit Parametertypen?
function animalToBird(a: Animal): Bird {
// ...
}
clone(animalToBird) // OK
function crowToBird(c: Crow): Bird {
// ...
}
clone(crowToBird) // TS2345: '(c: Crow) =>
// Bird' '
// (b: Bird) => Bird'.
Damit eine Funktion mit einer anderen Funktion kompatibel ist, müssen alle Parametertypen (einschließlich dieser)> sein: die entsprechenden Parameter in der anderen Funktion. Um zu verstehen, warum, überlegen Sie, wie der Benutzer crowToBird implementieren könnte, bevor Sie es an den Klon übergeben.
function crowToBird(c: Crow): Bird {
c.caw()
return new Bird
}
TSC-: STRICTFUNCTIONTYPES
- TypeScript this. , , {«strictFunctionTypes»: true} tsconfig.json.
{«strict»: true}, .
Wenn der Klon jetzt crowToBird mit neuem Vogel aufruft, erhalten wir eine Ausnahme, da .caw in allen Krähen, aber nicht in allen Vögeln definiert ist.
Dies bedeutet, dass Funktionen in ihren Parametern und diesen Typen kontravariant sind. Das heißt, eine Funktion kann nur dann ein Subtyp einer anderen Funktion sein, wenn jeder ihrer Parameter und Typ dies> sind: ihre entsprechenden Parameter in der anderen Funktion.
Glücklicherweise müssen diese Regeln nicht auswendig gelernt werden. Denken Sie daran, wenn der Editor eine rote Unterstreichung gibt, wenn Sie irgendwo eine falsch eingegebene Funktion übergeben.
Kompatibilität
Subtyp- und Supertyp-Beziehungen sind ein Schlüsselkonzept in jeder statisch typisierten Sprache. Sie sind auch wichtig, um zu verstehen, wie Kompatibilität funktioniert (denken Sie daran, Kompatibilität bezieht sich auf die TypeScript-Regeln für die Verwendung von Typ A, wenn Typ B erforderlich ist).
Wenn TypeScript die Frage "Ist Typ A mit Typ B kompatibel?" Beantworten muss, folgt es einfachen Regeln. Für Nicht-Enum-Typen - wie Arrays, Boolesche Werte, Zahlen, Objekte, Funktionen, Klassen, Klasseninstanzen und Zeichenfolgen, einschließlich Literaltypen - ist A mit B kompatibel, wenn eine der Bedingungen erfüllt ist.
- A <: B.
- A ist eine.
Regel 1 ist nur eine Subtypdefinition: Wenn A ein Subtyp von B ist, können Sie überall dort, wo B benötigt wird, A verwenden.
Regel 2 ist eine Ausnahme von Regel 1, um die Interaktion mit JavaScript-Code zu vereinfachen.
Bei Aufzählungstypen, die mit den Schlüsselwörtern enum oder const enum erstellt wurden, ist Typ A mit Aufzählung B kompatibel, wenn eine der Bedingungen erfüllt ist.
- A ist Mitglied der Aufzählung B.
- B hat mindestens ein Mitglied der Typennummer und A ist die Nummer.
Regel 1 ist genau die gleiche wie für einfache Typen (wenn A ein Mitglied von B ist, dann ist A vom Typ B und wir sagen B <: B).
Regel 2 ist erforderlich, um die Arbeit mit Aufzählungen zu vereinfachen, die die Sicherheit von TypeScript ernsthaft beeinträchtigen (siehe Unterabschnitt „Aufzählung“ auf Seite 60), und ich empfehle, sie zu vermeiden.
Typerweiterung Die Typerweiterung
ist der Schlüssel zum Verständnis der Funktionsweise der Typinferenz. TypeScript ist in der Ausführung nachsichtig und führt eher Fehler beim Ableiten allgemeinerer Typen als beim Ableiten möglichst spezifischer Typen durch. Dies erleichtert Ihnen das Leben und verkürzt den Zeitaufwand für die Bearbeitung der Notizen zur Typprüfung.
In Kapitel 3 haben Sie bereits einige Beispiele für die Typerweiterung gesehen. Betrachten Sie andere.
Wenn Sie eine Variable als veränderbar deklarieren (mit let oder var), wird ihr Typ vom Werttyp ihres Literals zum Basistyp erweitert, zu dem das Literal gehört:
let a = 'x' // string
let b = 3 // number
var c = true // boolean
const d = {x: 3} // {x: number}
enum E {X, Y, Z}
let e = E.X // E
Dies gilt nicht für unveränderliche Erklärungen:
const a = 'x' // 'x'
const b = 3 // 3
const c = true // true
enum E {X, Y, Z}
const e = E.X // E.X
Sie können explizite Typanmerkungen verwenden, um zu verhindern, dass sie erweitert werden:
let a: 'x' = 'x' // 'x'
let b: 3 = 3 // 3
var c: true = true // true
const d: {x: 3} = {x: 3} // {x: 3}
Wenn Sie einen nicht erweiterten Typ mit let oder var neu zuweisen, erweitert TypeScript ihn für Sie. Um dies zu verhindern, fügen Sie der ursprünglichen Deklaration eine explizite Typanmerkung hinzu:
const a = 'x' // 'x'
let b = a // string
const c: 'x' = 'x' // 'x'
let d = c // 'x'
Mit null oder undefiniert initialisierte Variablen werden auf Folgendes erweitert:
let a = null // any
a = 3 // any
a = 'b' // any
Wenn jedoch eine auf null oder undefiniert initialisierte Variable den Bereich verlässt, in dem sie deklariert wurde, weist TypeScript ihr einen bestimmten Typ zu:
function x() {
let a = null // any
a = 3 // any
a = 'b' // any
return a
}
x() // string
Der const-
Typ Der const- Typ hilft zu vermeiden, die Typdeklaration zu erweitern. Verwenden Sie es als Typzusicherung (siehe Unterabschnitt "Typgenehmigungen" auf Seite 185):
let a = {x: 3} // {x: number}
let b: {x: 3} // {x: 3}
let c = {x: 3} as const // {readonly x: 3}
const eliminiert die Typerweiterung und markiert seine Mitglieder rekursiv als schreibgeschützt, selbst in tief verschachtelten Datenstrukturen:
let d = [1, {x: 2}] // (number | {x: number})[]
let e = [1, {x: 2}] as const // readonly [1, {readonly x: 2}]
Verwenden Sie als const, wenn TypeScript den engstmöglichen Typ ableiten soll.
Überprüfen auf zusätzliche Eigenschaften Die
Typerweiterung kommt auch ins Spiel, wenn TypeScript prüft, ob ein Objekttyp mit einem anderen Objekttyp kompatibel ist.
Objekttypen sind in ihren Elementen kovariant (siehe Unterabschnitt „Variation von Form und Array“ auf Seite 148). Wenn TypeScript diese Regel jedoch ohne zusätzliche Überprüfungen befolgt, können Probleme auftreten.
Stellen Sie sich beispielsweise ein Options-Objekt vor, das Sie an eine Klasse übergeben können, um es anzupassen:
type Options = {
baseURL: string
cacheSize?: number
tier?: 'prod' | 'dev'
}
class API {
constructor(private options: Options) {}
}
new API({
baseURL: 'https://api.mysite.com',
tier: 'prod'
})
Was passiert jetzt, wenn Sie bei der Option einen Fehler machen?
new API({
baseURL: 'https://api.mysite.com',
tierr: 'prod' // TS2345: '{tierr: string}'
}) // 'Options'.
//
// , 'tierr'
// 'Options'. 'tier'?
Dies ist ein häufiger JavaScript-Fehler, und es ist gut, dass TypeScript Ihnen dabei hilft, ihn zu erkennen. Aber wenn die Objekttypen in ihren Mitgliedern kovariant sind, wie fängt TypeScript sie ab?
Mit anderen Worten:
- Wir haben den Typ {baseURL: string, cacheSize ?: Number, tier ?: 'Prod' | erwartet 'dev'}.
- Wir haben den Typ {baseURL: string, tierr: string} übergeben.
- Der übergebene Typ ist ein Subtyp des erwarteten Typs, aber TypeScript konnte einen Fehler melden.
Durch die Überprüfung auf zusätzliche Eigenschaften , wenn Sie versuchen , eine neue Objektliteral Typ T auf einem anderen Typen zuweisen, U und T hat Eigenschaften , die U nicht hat, meldet Typoskript einen Fehler.
Der neue Objektliteraltyp ist der Typ, den TypeScript aus einem Objektliteral abgeleitet hat. Wenn dieses Objektliteral eine Typzusicherung verwendet (siehe Unterabschnitt „Typzusicherungen“ auf Seite 185) oder einer Variablen zugewiesen ist, wird der neue Typ zum regulären Objekttyp erweitert und seine Neuheit geht verloren.
Versuchen wir, diese Definition umfangreicher zu gestalten:
type Options = {
baseURL: string
cacheSize?: number
tier?: 'prod' | 'dev'
}
class API {
constructor(private options: Options) {}
}
new API({ ❶
baseURL: 'https://api.mysite.com',
tier: 'prod'
})
new API({ ❷
baseURL: 'https://api.mysite.com',
badTier: 'prod' // TS2345: '{baseURL:
}) // string; badTier: string}'
// 'Options'.
new API({ ❸
baseURL: 'https://api.mysite.com',
badTier: 'prod'
} as Options)
let badOptions = { ❹
baseURL: 'https://api.mysite.com',
badTier: 'prod'
}
new API(badOptions)
let options: Options = { ❺
baseURL: 'https://api.mysite.com',
badTier: 'prod' // TS2322: '{baseURL: string;
} // badTier: string}'
// 'Options'.
new API(options)
❶ Instanziieren Sie die API mit baseURL und einer von zwei optionalen Eigenschaften: tier. Alles arbeitet.
❷ Wir schreiben fälschlicherweise Tier als badTier. Das Optionsobjekt, das wir an die neue API übergeben, ist neu (sein Typ wird abgeleitet, es ist nicht mit der Variablen kompatibel und wir geben keine Zusicherungen dafür ein). Wenn Sie also unnötige Eigenschaften überprüfen, erkennt TypeScript eine zusätzliche badTier-Eigenschaft (die im Optionsobjekt definiert ist, aber nicht in Typ Optionen).
❸ Geben Sie eine Erklärung ab, dass das ungültige Optionsobjekt vom Typ Options ist. TypeScript betrachtet es nicht mehr als neu und schließt mit der Überprüfung zusätzlicher Eigenschaften, dass keine Fehler vorliegen. Die as T-Syntax wird im Abschnitt "Typzusicherungen" auf S. 22 beschrieben. 185.
❹ Zuweisen des Optionsobjekts zur Variablen badOptions. TypeScript nimmt es nicht mehr als neu wahr und kommt nach Überprüfung auf unnötige Eigenschaften zu dem Schluss, dass keine Fehler vorliegen.
❺ Wenn wir Optionen explizit als Optionen eingeben, ist das Objekt, das wir Optionen zuweisen, neu. Daher sucht TypeScript nach zusätzlichen Eigenschaften und findet einen Fehler. Beachten Sie, dass in diesem Fall die Überprüfung auf zusätzliche Eigenschaften nicht durchgeführt wird, wenn Optionen an die neue API übergeben werden, sondern wenn wir versuchen, der Optionsvariablen ein Optionsobjekt zuzuweisen.
Sie müssen sich diese Regeln nicht merken. Dies ist nur eine interne TypeScript-Heuristik, um so viele Fehler wie möglich zu erkennen. Denken Sie daran, wenn Sie sich plötzlich fragen, wie TypeScript von einem Fehler erfahren hat, den selbst Ivan - ein Oldtimer Ihres Unternehmens und auch ein professioneller Code-Zensor - nicht bemerkt hat.
Die
TypeScript- Verfeinerung führt eine symbolische Ausführung der Typinferenz durch. Das Modul zur Typprüfung verwendet Befehlsablaufanweisungen (wie if,?, || und switch) zusammen mit Typabfragen (wie typeof, instanceof und in), wodurch die Typen qualifiziert werden, wenn ein Programmierer den Code liest. Diese praktische Funktion wird jedoch in sehr wenigen Sprachen unterstützt.
Stellen Sie sich vor, Sie haben eine API zum Definieren von CSS-Regeln in TypeScript entwickelt und Ihr Kollege möchte damit die Breite eines HTML-Elements festlegen. Es vermittelt die Breite, die Sie später analysieren und überprüfen möchten.
Implementieren wir zunächst eine Funktion zum Parsen einer CSS-Zeichenfolge in Wert und Einheit:
//
// , CSS
type Unit = 'cm' | 'px' | '%'
//
let units: Unit[] = ['cm', 'px', '%']
// . . null,
function parseUnit(value: string): Unit | null {
for (let i = 0; i < units.length; i++) {
if (value.endsWith(units[i])) {
return units[i]
}
}
return null
}
Wir verwenden dann parseUnit, um die vom Benutzer angegebene Breite zu analysieren. width kann eine Zahl (möglicherweise in Pixel) oder eine Zeichenfolge mit angehängten Einheiten oder null oder undefiniert sein.
In diesem Beispiel verwenden wir die Typqualifizierung mehrmals:
type Width = {
unit: Unit,
value: number
}
function parseWidth(width: number | string | null |
undefined): Width | null {
// width — null undefined, .
if (width == null) { ❶
return null
}
// width — number, .
if (typeof width === 'number') { ❷
return {unit: 'px', value: width}
}
// width.
let unit = parseUnit(width)
if (unit) { ❸
return {unit, value: parseFloat(width)}
}
// null.
return null ❹
}
❶ TypeScript kann verstehen, dass die lose Gleichheit von JavaScript mit null sowohl für null als auch für undefined true zurückgibt. Er weiß auch, dass wenn der Scheck bestanden wird, wir eine Rückgabe machen werden, und wenn wir keine Rückgabe machen, dann ist die Prüfung fehlgeschlagen und von diesem Moment an ist der Breitentyp Nummer | Zeichenfolge (kann nicht mehr null oder undefiniert sein). Wir sagen, dass der Typ von Nummer | verfeinert wurde Zeichenfolge | null | undefiniert in Nummer | Zeichenfolge.
❷ Bei der Prüfung wird zur Laufzeit nach einem Wert gefragt, um dessen Typ anzuzeigen. TypeScript nutzt auch typeof zur Kompilierungszeit: In dem if-Zweig, in dem der Test bestanden wird, weiß TypeScript, dass width number ist. Andernfalls (wenn dieser Zweig zurückgibt) sollte width string sein - der einzige verbleibende Typ.
❸ Da parseUnit null zurückgeben kann, überprüfen wir dies. TypeScript weiß, dass wenn die Einheit korrekt ist, sie im if-Zweig vom Typ Unit sein muss. Andernfalls ist die Einheit ungültig, was bedeutet, dass ihr Typ null ist (verfeinert von Unit | null).
❹ Schließlich geben wir null zurück. Dies kann nur passieren, wenn der Benutzer eine Zeichenfolge für die Breite übergibt, diese Zeichenfolge jedoch nicht unterstützte Einheiten enthält.
Ich ging den Gedankengang von TypeScript für jede vorgenommene Typverfeinerung durch. TypeScript macht einen großartigen Job, indem es Ihre Argumentation beim Lesen und Schreiben von Code aufgreift und ihn in Typprüfung und Inferenzreihenfolge kristallisiert.
Diskriminierte Join-Typen
Wie wir gerade herausgefunden haben, hat TypeScript ein gutes Verständnis für die Funktionsweise von JavaScript und kann unsere Typqualifikationen verfolgen, als ob wir unsere Gedanken lesen würden.
Angenommen, wir erstellen ein benutzerdefiniertes Ereignissystem für eine Anwendung. Wir beginnen mit der Definition der Ereignistypen zusammen mit den Funktionen, die das Eintreffen dieser Ereignisse behandeln. Stellen Sie sich vor, UserTextEvent simuliert ein Tastaturereignis (z. B. hat der Benutzer den Text <input /> eingegeben) und UserMouseEvent simuliert ein Mausereignis (der Benutzer hat die Maus an den Koordinaten bewegt [100, 200]):
type UserTextEvent = {value: string}
type UserMouseEvent = {value: [number, number]}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
if (typeof event.value === 'string') {
event.value // string
// ...
return
}
event.value // [number, number]
}
TypeScript weiß, dass event.value im if-Block eine Zeichenfolge sein muss (dank des typeof-Checks), dh event.value nach dem if-Block muss ein Tupel [number, number] sein (aufgrund der Rückgabe im if-Block).
Wohin führen Komplikationen? Fügen wir den Ereignistypen Erläuterungen hinzu:
type UserTextEvent = {value: string, target: HTMLInputElement}
type UserMouseEvent = {value: [number, number], target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
if (typeof event.value === 'string') {
event.value // string
event.target // HTMLInputElement | HTMLElement (!!!)
// ...
return
}
event.value // [number, number]
event.target // HTMLInputElement | HTMLElement (!!!)
}
Während die Verfeinerung für event.value funktionierte, war dies für event.target nicht der Fall. Warum? Wenn handle einen Parameter vom Typ UserEvent empfängt, bedeutet dies nicht, dass Sie ihn entweder UserTextEvent oder UserMouseEvent übergeben müssen. Tatsächlich können Sie ein Argument vom Typ UserMouseEvent | übergeben UserTextEvent. Und da sich die Mitglieder einer Gewerkschaft überschneiden können, benötigt TypeScript eine zuverlässigere Methode, um zu wissen, wann und welcher Fall einer Gewerkschaft relevant ist.
Sie können dies tun, indem Sie Literaltypen und eine Tag-Definition für jeden Fall eines Vereinigungstyps verwenden. Netter Tag:
- In jedem Fall befindet es sich an derselben Stelle des Vereinigungstyps. Impliziert dasselbe Objektfeld beim Kombinieren von Objekttypen oder denselben Index beim Kombinieren von Tupeln. In der Praxis sind diskriminierte Gewerkschaften häufiger Objekte.
- Wird als Literaltyp eingegeben (Zeichenfolgenliteral, numerisch, boolesch usw.). Sie können verschiedene Arten von Literalen mischen und anpassen, aber es ist am besten, sich an einen einzelnen Typ zu halten. In der Regel handelt es sich hierbei um eine Art Zeichenfolgenliteral.
- Nicht universell. Tags dürfen keine generischen Typargumente erhalten.
- Sich gegenseitig ausschließen (innerhalb des Gewerkschaftstyps einzigartig).
Lassen Sie uns die Ereignistypen unter Berücksichtigung der oben genannten Punkte aktualisieren:
type UserTextEvent = {type: 'TextEvent', value: string,
target: HTMLInputElement}
type UserMouseEvent = {type: 'MouseEvent', value: [number, number],
target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
if (event.type === 'TextEvent') {
event.value // string
event.target // HTMLInputElement
// ...
return
}
event.value // [number, number]
event.target // HTMLElement
}
Wenn wir nun das Ereignis basierend auf dem Wert des mit Tags versehenen Felds (event.type) verfeinern, weiß TypeScript, dass sich im if-Zweig ein UserTextEvent befinden sollte, und nach dem if-Zweig sollte es sich um ein UserMouseEvent handeln. Da Tags in jedem Union-Typ eindeutig sind, weiß TypeScript dass sie sich gegenseitig ausschließen.
Verwenden Sie diskriminierte Verknüpfungen, wenn Sie eine Funktion schreiben, die verschiedene Fälle von Verknüpfungstypen behandelt. Wenn Sie beispielsweise mit Flux-Aktionen arbeiten, stellen Sie Redux wieder her oder verwenden Sie Reducer in React.
Sie können sich detaillierter mit dem Buch vertraut machen und es zu einem Sonderpreis auf der Website des Herausgebers vorbestellen