A Trip through the Type Jungle
Typescript hat mit 4.1 einige interessante Neuerungen erhalten. Wir hatten bereits über die Utility Types gesprochen. Möglich ist jetzt aber eine ganze Menge mehr. Die spannendsten Neuerungen sind unter anderem:
Recursive Types
Mit dieser Neuerung ist es möglich, dass Objekte sich selbst beinhalten dürfen. Betrachtet man beispielsweise eine verketteten Liste, dann hat jedes Element eine Referenz auf seinen Nachfolger:
interface Node<T> {
value: T;
next: Node<T>;
}
Wie ein späteres Beispiel zeigt, sollte man mit Rekursionen allerdings aufpassen, da sonst schnell der Compiler streikt.
Tuples
Variadic Tuples ermöglichen abstraktere Operationen auf Arrays und Tuplen ohne etwas über die Typisierung des Inhalts zu wissen. Ein paar gute Beispiele findet man in der Typescript Dokumentation (siehe unten).
Conditional Types
Dieses Feature erlaubt es Typen basierend auf anderen Typen zu definieren. Die Syntax TypA extends TypB ? TypC : TypD
erinnert an ternäre Operatoren aus anderen Programmiersprachen. Ein Beispiel aus der Typescript Dokumentation veranschaulicht, wozu man Conditional Types gebrauchen kann:
type Flatten<T> = T extends any[] ? T[number] : T;
// Extracts out the element type.
type Str = Flatten<string[]>; // = string
// Leaves the type alone.
type Num = Flatten<number>; // = number
Template Literal Types
Typescript beherrscht bereits String Literal Types:
type Color = 'red' | 'blue' | 'green';
Das ist sehr nützlich um die möglichen Werte eines Strings einzuschränken. Allerdings kommen String Literal Types schnell an ihre Grenzen, wenn man zusammengesetzte String benutzt. Beispiel: Positionieren von Elementen
type Vertical = 'top' | 'center' | 'bottom';
type Horizontal = 'left' | 'center' | 'right';
type Position = 'top-left' | 'top-center' | 'top-right' | 'center-left' | ...;
Template Literal Types können hierbei Abhilfe schaffen. Sie funktionieren genauso wie Template Strings in Programmiersprachen. Aus dem obigen Beispiel können wir also folgendes machen:
type Vertical = 'top' | 'center' | 'bottom';
type Horizontal = 'left' | 'center' | 'right';
type Position = `${Vertical}-${Horizontal}`;
Spannend hierbei ist, dass Typescript automatisch alle möglichen Kombinationen von Vertical und Horizontal errechnet.
Real World Usecase
Ähnlich zu der lodash Funktion _.get
haben wir in einem Projekt eine Funktion die auf Properties von Objekten zugreift. Dabei sind das Objekt und der Pfad zu der gewünschten Property Eingabeparameter der Funktion:
function get(obj, path) {
// access obj[path] and return property
}
get(complexObject, 'some.deeply.nested.property');
Für diese Funktion kann man jetzt eine recht simple Typisierung einführen:
function get<T>(obj: T, path: string): any {
// access obj[path] and return property
}
get(complexObject, 'some.deeply.nested.property');
Die Funktion ist nun generisch und der path
Parameter hat den Typ string
erhalten. Es wäre auch denkbar keyof T
als Typ zu benutzen, hat allerdings den Nachteil, dass es dann nicht möglich ist, tiefer gelegene Properties zu adressieren. Abhilfe schaffen die oben genannten Neuerungen von Typescript. Dazu benötigen wir erst einmal ein paar Hilfstypen:
// utility type for controlling recursion depth; Prev[D] => D-1 (or never at last)
// examples:
// Prev[4] => 3
// Prev[0] => never
type Prev = [never, 0, 1, 2, 3, 4, ...0[]];
// joins two strings/numbers with a dot
// examples:
// Join<"a", "b.c"> => "a.b.c"
// Join<"a", ""> => "a"
type Join<K, P> = K extends string | number
? P extends string | number
? `${K}${'' extends P ? '' : '.'}${P}`
: never
: never;
// traverses the type T down upto 3 levels deep and creates path types for each property by joining their name together with a dot (.).
// 3 levels of nesting is the maximum for the compiler to not complain.
type Leaves<T, D extends number = 10> = [D] extends [never]
? never
: T extends object
? {
[K in keyof T]-?: Join<K, Leaves<T[K], Prev[D]>>
}[keyof T]
: '';
type Paths<T, D extends number = 3> = [D] extends [never]
? never
: T extends object
? {
[K in keyof T]-?: K extends string | number
? `${K}` | Join<K, Paths<T[K], Prev[D]>>
: never
}[keyof T]
: ''
Mit diesen Hilfstypen können wir unsere Funktion nun strenger typisieren:
function get<T>(obj: T, path: Leaves<T>): any {
// access obj[path] and return property
}
const complexObject: ComplexType = {
id: '1',
some: {
deeply: {
nested: {
property: 1337,
},
},
},
};
get<ComplexType>(complexObject, 'some.deeply.nested.unknown.property'); // -> type error
get<ComplexType>(complexObject, 'some.deeply.nested.property');
Dokumentation:
Quellen
Standort Hannover
newcubator GmbH
Bödekerstraße 22
30161 Hannover
Standort Dortmund
newcubator GmbH
Westenhellweg 85-89
44137 Dortmund