16.6.2021 |

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:

Tuple Types

Conditional Types

Recursive Types

Template Literal Types

Quellen

Hilfstypen

Zur Übersicht

Mehr vom Devsquad...

Umut Tufanoglu

Cypress Visible deprecated

Simon Jakubowski

String.formatted