JavaScript verfügt über ein dynamisches Typsystem. Variablen und Funktionsparameter können darin beliebige Werte, aber auch beliebige Typen annehmen. Die explizite Angabe von Typen ist nicht möglich. Das bedeutet einerseits eine große Freiheit und viel Flexibilität, andererseits ist der Ansatz aber auch fehleranfällig und führt sowohl bei der Weiterentwicklung als auch bei der Verwendung von Code schnell zu Problemen.
Dieses Problem adressiert TypeScript, das ein statisches Typsystem für JavaScript zur Verfügung stellt. Um der Komplexität, die sich aus dem dynamischen Typsystem von JavaScript ergibt, zu begegnen, kann man in TypeScript nicht nur normale Typannotationen hinschreiben, sondern mit einer Art Metaprogrammiersprache auch programmatisch Typen beschreiben. Diese Metasprache wird im Folgenden anhand einer aus technischer Sicht typischen JavaScript-Funktion erläutert, für die die korrekten Typen entwickelt werden sollen. Der Quellcode zu diesem Artikel ist auch online in einem „TypeScript Playground“ unter [1] zu finden.
Listing 1 zeigt die Funktion, deren fachliche Idee es ist, ein Objekt mit Werten zu überprüfen. Sie soll damit exemplarisch für Funktionen stehen, die mehr oder weniger beliebige Daten entgegennehmen und in veränderter Form zurückliefern. Die validate-Funktion erwartet zwei JavaScript-Objekte, einerseits ein Objekt values, das die Menge der zu prüfenden Werte enthält. Der zweite Parameter ist ebenfalls ein JavaScript-Objekt, rules, das Callback-Funktionen enthält, mit denen die validate-Funktion die einzelnen Werte des values-Objekts überprüfen kann. Die Implementierung der validate-Funktion würde für jeden Eintrag im values-Objekt die validate-Funktion für die entsprechende Regelfunktion aus dem rules-Objekt aufrufen. Diese liefert dann jeweils das Prüfergebnis für einen Eintrag zurück (true oder false, je nachdem, ob der Wert gültig ist oder nicht). Wenn unter einem Key im values-Objekt eine Funktion abgelegt ist, soll deren Rückgabewert an die rules-Funktion übergeben werden.
Damit die validate-Funktion weiß, welche Funktion im rules-Objekt zu welchem Feld im values-Objekt gehört, sollen diese Funktionen per Konvention unter einem Key abgelegt sein, der ähnlich heißt wie der zugehörige Key im values-Objekt: Der Name soll dem Muster validateFeldname folgen, also beginnend mit dem String validate und gefolgt von dem Key-Namen mit großem Anfangsbuchstaben. Wenn es also im rules-Objekt einen Eintrag firstname gibt, der auf den Typ string gesetzt ist (bzw. dessen Wert vom Typ string ist), erwartet die validate-Funktion im rules-Objekt unter dem Key validateFirstname eine Funktion, die einen Parameter vom Typ string entgegennehmen kann und die ihrerseits ein boolean zurückliefert. Gibt es diese Funktion im rules-Objekt nicht, soll der Wert ohne Prüfung als gültig gelten.
Die validate-Funktion liefert dann ein Objekt, in dem die Ergebnisse der Prüfungen enthalten sind. Darin entsprechen die Namen der Keys den Keys im values-Objekt und die Werte sind jeweils das Ergebnis der Validierung (true oder false).
Listing 1
function validate(values, rules) {
// Implementierung ausgelassen
}
const person = {
firstname: "Klaus",
age: 32,
sallary() { return 60000 }
}
const personRules = {
validateFirstname(s) {
return s.length > 3
},
validateSallary(n) {
return n > 1000;
}
}
const result = validate(person, personRules);
if (result.firstname === false) {
// firstname ungültig
}
if (result.sallary === true) {
// salary gültig
}
In TypeScript können die oben beschriebenen Regeln auf Typebene mit einem Mapped Type beschrieben werden. Damit können Regeln ausgedrückt werden, wie: „Welche Keys auch immer das Objekt enthält, das der validate-Funktion als ersten Parameter übergeben wird, der Rückgabetyp sieht so aus, dass darin alle Keys des übergebenen Objekts vorhanden sind, die Werte darin aber vom Typ boolean sind.“ Oder: „Der zweite Parameter soll ein Objekt sein, in dem Keys enthalten sind, die der genannten Namenskonvention entsprechen – und keine darüber hinausgehenden Keys.“
Dadurch weiß TypeScript dann, und zwar ohne dass dafür für jedes zu validierende Objekt Typen manuell geschrieben werden müssten, wie Funktionsargumente und Rückgabetyp aussehen. Mit dieser Information kann TypeScript dann (wie bei einem explizit definierten Typ) die korrekte Verwendung überprüfen und bei der Entwicklung mit Code Completion etc. assistieren. Das Konzept der Mapped Types ist sehr mächtig und wird im Folgenden Schritt für Schritt an eigenen Beispielen vorgestellt. Zum Schluss kann dann die validate-Funktion komplett typsicher beschrieben werden.
LUST AUF NOCH MEHR JAVASCRIPT?
Entdecken Sie Workshops vom 17. - 20. März 2025
Generics
Um allgemeine Typen oder allgemeine Funktionssignaturen zu beschreiben, werden in TypeScript wie auch in anderen Programmiersprachen Generics verwendet. Damit können an einer Funktion Typvariablen deklariert werden, die beispielsweise den Typ eines Funktionsarguments annehmen und an anderen Stellen in der Funktionssignatur wiederverwendet werden können. Dazu ein Beispiel: Eine fiktive Funktion saveToDatabase soll einen beliebigen Wert in eine Datenbank schreiben. Möglicherweise verändert sie dabei diesen Wert auch (zum Beispiel durch Hochsetzen eines Versionszählers) und liefert den aktualisierten Wert an den Aufrufer zurück. Grundsätzlich könnte die Methodensignatur in TypeScript den any-Typ verwenden, an den alle Werte zugewiesen werden können (Listing 2). Dieser Typ kann sowohl für das Funktionsargument als auch für den Rückgabetyp verwendet werden.
Das Problem: TypeScript weiß dann beim Rückgabetyp nicht, um welchen genauen Typ es sich handelt. Präziser ist es, auszudrücken, dass der Funktionsparameter zwar ein beliebiger Typ sein darf, aber der Rückgabetyp ebenfalls genau diesem Typ entspricht. Dazu kann mit einer Typvariable eine generische Funktion beschrieben werden (Listing 3). Die Typvariablen werden in spitzen Klammern deklariert und können dann in der Funktionssignatur überall verwendet werden, wo TypeScript eine Typangabe erwartet. Hier also im Funktionsparameter und als Rückgabetyp.
Listing 2
function saveToDatabase(value: any): any { /* Implementierung ausgelassen */ }
const result = saveToDatabase({firstname: "Klaus"});
// result ist hier any, sodass keine weitere Typprüfung stattfindet
result.firstname; // OK
result.lastname; // Kein Compile-Fehler, obwohl lastname nicht am Objekt definiert ist
function saveToDatabase<O>(value: O): O { /* Implementierung ausgelassen */ };
const result = saveToDatabase({ firstname: "Klaus" });
result.firstname; // OK
result.lastname; // ERR: Property 'lastname' does not exist
Die saveToDatabase-Funktion kann nun mit beliebigen Typen aufgerufen werden, also mit Objekten, primitiven Typen wie string oder number, aber auch mit null oder undefined. Nicht immer ist dieses Verhalten gewünscht. In vielen Fällen gibt es Restriktionen, welche Typen verwendet werden dürfen. Im Fall der saveToDatabase-Funktion wäre es denkbar, dass diese nur mit Objekten arbeiten kann. Dazu können bei einer Typvariable mit dem Schlüsselwort extends Restriktionen beschrieben werden, die bei der Verwendung der Funktion bzw. der Typvariable eingehalten werden müssen. Listing 4 zeigt zwei weitere Beispiele möglicher Signaturen für die saveToDatabase-Funktion. Die erste Signatur gibt an, dass der Funktion ein beliebiges Objekt übergeben werden darf (aber eben kein string, number, null etc.). Die zweite Signatur schränkt die Vorgabe noch weiter ein und drückt aus, dass das übergebene Objekt mindestens eine Eigenschaft id haben muss, die vom Typ string sein muss.
Listing 4
function saveToDatabase<O extends object>(value: O): O { /* ... */ };
saveToDatabase("Klaus"); // Fehler: "Klaus" ist kein object
function saveToDatabase<O extends { id: string }>(value: O): O { /* ... */ };
saveToDatabase({
// Fehler: id-Property fehlt
firstname: "Klaus"
})
Der keyof-Operator
Ein gängiger Anwendungsfall ist, dass an eine Funktion nicht nur ein Objekt, sondern ein String mit einem gültigen Key aus dem Objekt übergeben werden soll. Auch diese Einschränkung lässt sich mit TypeScript abbilden, sodass es zu einem Compile-Fehler kommt, wenn der Funktion ein String übergeben wird, der nicht dem Namen eines Keys entspricht.
Listing 5 zeigt eine addListener-Funktion, die in der Lage sein soll, eine Listener-Funktion (z. B. einen Event Handler) für einen angegebenen Eintrag in einem beliebigen Objekt zu registrieren. Dazu werden ihr drei Parameter übergeben: das Objekt, der Name eines Keys aus dem Objekt und die eigentliche Listener-Funktion, die für dieses Feld registriert werden soll.
Listing 5
function addListener<O extends object>(
object: O,
key: string,
listenerFn: Function) { }
const person = {
firstname: "Klaus",
age: 32
}
addListener(person, "firstname", (newValue) => { /*... */ });
addListener(person, "age", (newValue) => { /*... */ });
// Dieser Aufruf sollte einen Compile-Fehler ergeben:
addListener(person, "lastname", (newValue) => { /*... */ });
Listing 6
type Red = "red";
type Black = "black";
// Variablen, Argumente etc. vom Typ Red können jetzt nur den String// "red" annehmen, alles andere führt zu Compile-Fehlern
const redColor: Red = "red"; // OK
const blackColor: Red = "black" // ERR
type Colors = Red | Black;
const r: Colors = "red"; // OK
const b: Colors = "black"; // OK
const g: Colors = "grey"; // ERROR
Listing 7
const person = {
firstname: "Klaus",
age: 32
}
type KeysOfKlaus = keyof typeof person;
// "firstname" | "age"
Listing 8
function addListener<O extends object, K extends keyof O>(
obj: O,
name: K,
listener: Function
) {
// ...
}
const person = {
firstname: "Klaus", age: 32
};
addListener(person, "firstname", v => { /* ... */ }); // OK
addListener(person, "lastname", v => { /* ... */ }); // ERR:
// Argument of type "lastname" is not // assignable to parameter of type "firstname" // | "age"
Index Accessed Type
Die an addListener übergebene Callback-Funktion (dritter Parameter) wird aufgerufen, sobald sich der Wert im Objekt, für den sie registriert wurde, verändert. Der neue Wert soll ihr dann von addListener übergeben werden. Aus diesem Grund muss die Callback-Funktion über genau ein Argument verfügen. Und dieses Argument muss vom selben Typ sein, den auch der zugehörige Eintrag im überwachten Objekt hat. Für den Key “firstname”, der vom Typ string ist, muss eine Listener-Funktion übergeben werden, die ein Argument vom Typ string hat. Für das Feld “age” müsste die Callback-Funktion ein Argument vom Typ number haben.
In der Signatur der gezeigten addListener-Funktion in Listing 8 wird für den Parameter, mit dem die Listener-Funktion übergeben wird, zurzeit noch der Typ Function verwendet, der eine beliebige Funktion beschreibt und keine Aussage über Funktionsargumente oder -rückgabewerte macht. Funktionen können mit TypeScript aber auch präzise beschrieben werden. Die Callback-Funktion für den firstname-Listener, die genau ein Argument vom Typ string erwartet und nichts zurückliefert, kann wie folgt beschrieben werden: (newValue: string) => void.
Um das Argument für eine Callback-Funktion für addListener korrekt zu typisieren, benötigen wir den Typ, den der zugehörige Eintrag im übergebenen Objekt hat. Typen von Einträgen in einem Objekt können mit einem Index Accessed Type ermittelt werden. Die Notation ist dabei identisch mit dem Indexoperator in JavaScript, nur dass man kein Objekt angibt, sondern einen Typ (Listing 9). Da wir in der Signatur der addListener-Funktion sowohl den Typ des Objekts (Typvariable O) als auch den Namen des Keys (Typvariable K) daraus kennen, können wir folglich den Typ aus dem übergebenen Objekt für den übergebenen Key abfragen und diesen für die Beschreibung der Signatur der erwarteten Listener-Funktion verwenden (Listing 10). Damit ist die addListener-Funktion vollständig beschrieben. Der Aufrufer dieser Funktion hat volle Typsicherheit, ohne beim Verwenden selbst einen einzigen Typ angeben oder gar beschreiben zu müssen!
Listing 9
const person = {
firstname: "Klaus",
age: 32,
address: { city: "Hamburg" }
};
type Person = typeof person;
type Age = Person["age"]; // number
type Address = Person["address"]; // { city: string }
function addListener<O extends object, K extends keyof O>(
obj: O,
name: K,
listener: (newValue: O[K]) => void
) {
// ...
}
// Beim Verwenden der addListener-Funktion ist keine Angabe eines Typs erforderlich:
const person = {
firstname: "Klaus",
age: 32,
};
addListener(person, "firstname", (newValue) => { /* ... */ }); // OK
// Dieser Aufruf geht nicht:
// ERR: Argument of type "lastname" is not assignable to parameter of type "firstname" // | "age"
addListener(person, "lastname", (newValue) => { /* ... */ });
function ageChangeListener(newAge: number) { /* ... */ }
addListener(person, "firstname", ageChangeListener);
// ERR: Argument of type '(newAge: number) => void' is not assignable to parameter of // type '(newValue: string) => void'.
// Types of parameters 'newAge' and 'newValue' are incompatible.
Mapped Types
Zurück zur eingangs beschriebenen validate-Funktion. Diese soll ein Objekt zurückliefern, das dieselben Keys wie das übergebene values-Objekt enthält. Die Werte darin sollen allerdings jeweils vom Typ boolean sein. Eine solche Regel lässt sich in TypeScript mit Mapped Types ausdrücken. Ein Mapped Type nimmt einen bestehenden Typ und bildet ihn auf einen neuen Typ ab. Der bestehende Typ kann entweder ein festkodierter oder, wie im Fall der validate-Funktion notwendig, ein generischer Typ sein. Unabhängig davon kann in der Beschreibung des Mapped Type dann über die Keys des Ausgangstyps mit keyof iteriert werden und jedem Eintrag dabei ein neuer Typ zugewiesen werden.
Eine erste Variante für die Typbeschreibung der validate-Funktion mit einem Mapped Type findet sich in Listing 11. Hier wird sichergestellt, dass das übergebene rules-Objekt eine Untermenge des übergebenen values-Objekts ist (durch das Hinzufügen des Fragezeichens im Mapped Type werden die Einträge optional gemacht). Die Einträge, die gesetzt sind, müssen dann aber eine Funktion sein, die als Parameter einen Typ mit demselben Wert wie im values-Objekt entgegennimmt (ähnlich wie im addListener-Beispiel). Um die Anforderung, dass die Keys auch umbenannt werden sollen (validateFeldname), kümmern wir uns später. Der Rückgabetyp ist sogar schon vollständig gemäß den Anforderungen beschrieben, nach denen das zurückgegebene Objekt alle Keys aus dem values-Objekt enthält, deren Werte aber jeweils vom Typ boolean sind.
Hier kann TypeScript die korrekte Angabe des rules-Objekts sowie die korrekte Verwendung des Rückgabetyps sicherstellen, ohne dass dafür vom Aufrufer der validate-Funktion eine Typangabe erfolgen müsste.
Listing 11
type Rules<O extends object> = {
[K in keyof O]?: (value: O[K]) => boolean;
};
type ValidatedObject<O extends object> = {
[K in keyof O]: boolean;
};
function validate<O extends object>(
values: O, rules: Rules<O>
): ValidatedObject<O> {
return /* ... */;
}
const person = {
firstname: "Klaus", age: 32
}
const result = validate(person, {
firstname(v) {
// v ist hier korrekt als string
return v.length > 3
},
age(a) {
// a ist hier korrekt als number
return a > 0;
}
})
result.age; // OK, boolean
result.lastname; // ERR: lastname gibt‘s nicht // an result
// Auch OK: keine rule-Funktion für 'age'
validate(person, {
firstname(v) {
return v.length > 3
},
});
// Der Typ des Rule-Objekts für ein konkretes // Objekt lässt sich auch ermitteln, um diesen// bei Bedarf explizit angeben zu können:
type PersonRules = Rules<typeof person>;
const rules: PersonRules = {
firstname(n) { return n.length > 3; }, // OK
lastname(l) { return true } // ERR: lastname // nicht in 'person'
}
Conditional Types
Wenn in einem übergebenen values-Objekt unter einem Key eine Funktion abgelegt ist, soll die validate-Funktion beim Validieren diese Funktion aufrufen und ihren Rückgabewert an die zugehörige rules-Funktion übergeben. In diesem Fall muss also der Typ im rules-Objekt dem Rückgabetyp der Funktion im values-Objekt entsprechen (Listing 12)
Listing 12
const person = {
firstname: "Klaus",
salutation: function () {
"Hello, " + this.firstname;
}
};
// Erwarteter Typ des zugehörigen rules-Objekt:
type RulesForPerson = {
firstname?: (newValue: string) => boolean;
// person.salutation gibt einen string zurück, // folglich muss hier newValue auch string sein:
salutation?: (newValue: string) => boolean;
};
Listing 13
function getLength<O extends string | null>(s: O)
: O extends string ? number : null
{ /* ... */ }
const l = getLength("Moin!"); // l ist // "number"
const n = getLength(null); // n ist "null"
const x = getLength(7); // ERR: 7 ist // kein string
Listing 14
type Rules<O extends object> = {
[K in keyof O]?: O[K] extends Function
? (value: string) => boolean
: (value: O[K]) => boolean;
};
// ValidatedObject und validate-Function // unverändert
const person = {
firstname: "Klaus",
salutation() {
return "Hello, " + this.firstname;
}
}
validate(person, {
firstname(f) { return f.length > 3 },
salutation(s) {
// s ist hier 'string' (nicht 'Function' // wie in 'person')
return s.length > 7;
}
});
Listing 15
type GetReturnType<O> = O extends (...args: any) => infer R ? R : O
function sayHello() { return "Hello" };
type SayHelloReturn = GetReturnType<typeof sayHello>; // string
function sayNothing() { };
type SayNothingReturn = GetReturnType<typeof sayNothing>; // void
type NotAFunction = GetReturnType<string>; // string
Listing 16
type RuleFunction<O extends object, K extends keyof O> =
O[K] extends (...args: any) => infer R
? (value: R) => boolean
: (value: O[K]) => boolean;
type Rules<O extends object> = {
[K in keyof O]?: RuleFunction<O, K>
};
// ...ValidatedObject und validate-// Function unverändert...
const person = {
firstname: "Klaus",
salutation() { // liefert string zurück
return "Hello, " + this.firstname;
},
sallary() { // liefert number zurück
return 123456;
}
}
validate(person, {
salutation(s) {
// s ist hier ‘string’
return s.length > 7;
},
sallary(n) {
// n ist hier korrekt number
return n > 1000;
}
});
Template Literal Types
Es sind jetzt fast alle Anforderungen an die Typen der validate-Funktion umgesetzt. Allerdings sind die erwarteten Key-Namen im rules-Objekt noch nicht korrekt. Diese sollen nicht feldname, sondern validateFeldname heißen (also validate und dann der Name des Keys aus dem values-Objekt mit führendem Großbuchstaben).
Zur Erinnerung: die keyof-Funktion liefert eine Menge konkreter Ausprägungen von Strings zurück (first-name, age etc.). Dabei handelt es sich aber nicht um Werte, sondern um Typen. (String-)Werte können in JavaScript mit einem Templatestring verändert werden. Dazu kann in einem Templatestring ein Platzhalter definiert werden. Ein vergleichbares Konzept gibt es in TypeScript auch auf Typebene. Ein sehr triviales Beispiel, das nur dazu dient, diese Syntax zu illustrieren, ist in Listing 17 zu sehen. Der sayHello-Funktion wird ein beliebiger String übergeben. Aus diesem String wird ein „Hallo“-Gruß erzeugt und zurückgeliefert. Der Rückgabetyp drückt das ebenfalls aus. Zurück liefert die Funktion nämlich nicht den allgemeinen Typ string, sondern den ganz konkreten String, der auch dem zurückgelieferten Wert entspricht.
Neben der sayHello-Funktion ist ein fachlich sinnvolleres Beispiel zu sehen. Die setSpacing-Funktion soll ein Spacing und eine Größenangabe entgegennehmen. Damit sollen, wie in CSS üblich, Abstände ausgedrückt werden (padding oder margin), die sich auf eine Seite (top, right etc.) beziehen. Außerdem soll der Abstand mit einer Einheit übergeben werden. Beide Argumente können mit einem Template Literal Type beschrieben werden.
Für das Spacing sollen zwei Listen mit konkreten Strings (Abstand und Seite) kombiniert werden. Wenn in einem Template Literal Type ein oder mehrere Union Types verwendet werden, besteht der erzeugte Typ aus einer Kombination aller Ausprägungen aller Union Types (margin-top, padding-top, margin-left, padding-left und so weiter). Für die Größe soll eine beliebige Zahl mit den Abständen angegeben werden. Dazu wird für die Einheit ebenfalls ein Union Type mit den erlaubten Abständen (px, em, rem) definiert, der hier mit dem allgemeinen number-Typ kombiniert wird, sodass alle möglichen Zahlen verwendet werden können. Die setSpacing-Funktion kann mit diesen Typangaben nun nur noch mit korrekten Werten aufgerufen werden, auch wenn es sich bei den Parametern aus JavaScript-Sicht um „normale“ Strings handelt.
Listing 17
function sayHello<S extends string>(s: S): `Hello, ${S}!` {
return `Hello, ${s}!`;
}
const s = sayHello("Susi"); // Typ von S: "Hello, Susi!"
type Spacing = "margin" | "padding";
type Direction = "top" | "right" | "bottom" | "left";
type Unit = "px" | "em" | "rem"
function setSpacing(s: `${Spacing}-${Direction}`, size: `${number}${Unit}`) {
/* ... */
}
setSpacing("margin-right", "2rem"); // OK
setSpacing("padding-center", "2rem"); // ERROR: "padding-center" ungültig
setSpacing("padding-left", "2pt"); // ERROR: "2pt" ungültig
// (Unit 'pt' gibt es nicht)
Um einen Key in einem Mapped Type umzubenennen, wird das Schlüsselwort as verwendet, mit dem in TypeScript ein Type Cast durchgeführt wird. In diesem Fall soll der bestehende Typ (Name des Keys) auf einen neuen Typ (umbenannter Key) gecastet werden: [K in keyof O as RuleFnName <K>]?: RuleFunction<O[K]>.
Leider ist der Aufruf der ValidateFnName-Funktion hier allerdings so nicht möglich. Dieser Typ erwartet nämlich einen String für die Typvariable (S extends string). Der Union Type, der von keyof zurückgeliefert wird, kann allerdings auch number– oder symbol-Typen enthalten. Um Keys dieser Typen auszuschließen, kann man einen Trick anwenden. Dazu wird der keyof-Ausdruck in einen Intersection Type umgeschrieben, der den Union Type mit den einzelnen Keys (keyof O) und dem allgemeinen Typ string zusammenführt. Ein Intersection Type enthält nur die (kompatiblen) Schnittmengen von zwei Typen. In unserem Fall werden dadurch alle Nichtstrings herausgefiltert. Die nun vollständige Typdefinition für die validate-Funktion ist in Listing 18 zu sehen.
Listing 18
type RuleFnName<S extends string> = `validate${Capitalize<S>}`
type RuleFunction<T> = T extends (
...args: any
) => infer R
? (value: R) => boolean
: (value: T) => boolean;
type Rules<O extends object> = {
[K in keyof O & string as RuleFn-Name<K>]?: RuleFunction<O[K]>;
};
// ValidatedObject und validate // unverändert
const person = {
firstname: "Klaus",
salutation() {
return "Hello, " + this.firstname;
},
sallary() {
return 123456;
},
}
const rules: Rules<typeof person> = {
validateFirstname(a) {
return a.length > 3;
},
validateSallary(n) {
return n > 100;
}
}
const result = validate(person, rules);
const firstNameValid: boolean = result.firstname; // OK
const sallaryValid: string = result.sallary; // ERR: Type ‘boolean’ is not assignable // to type 'string'
const addressValid = result.address; // ERR: Property 'address' does not exist // on type 'ValidatedObject<...>'
Fazit
TypeScript ist mehr als ein „normales“ statisches Typsystem. Mit seinen spezialisierten Typen und Konzepten wie Mapped Types, Conditional Types, Template Literal Types und Generics lassen sich dynamisch Typen erzeugen, mit denen auch komplexe Codestrukturen typsicher beschrieben werden können, die ihrerseits von den Möglichkeiten des dynamischen Typsystems von JavaScript Gebrauch machen.
Der Einsatz dieser „Metasprache“ lohnt sich insbesondere bei Code, der ein API darstellt und von vielen Verwendern genutzt wird. Diese profitieren dann nämlich von typsicherem Code, ohne selbst Typdefinition schreiben zu müssen. Für einige gängige Anforderungen stehen bereits fertige Utility-Typen zur Verfügung, die in der TypeScript-Dokumentation beschrieben sind.