Cheat Sheet

Typ-Level-Programmierung mit TypeScript

Typ-Level-Programmierung mit TypeScript

Cheat Sheet

Typ-Level-Programmierung mit TypeScript


Typinferenz

// TypeScript ermittelt Datentypen automatisch
let a = 1; // a: number
let b = "Hello"; // b: string
let c = [1, 2, 3]; // c: number[]
let d = { foo: [1, 2, 3] }; // d: { foo: number[] }
Web

Pro-Tipp!

Typannotationen werden in Funktions- und Methodensignaturen sowie in Klassendefinitionen immer benötigt, ansonsten nur dort, wo TypeScript etwas nicht oder falsch versteht. Die Compiler-Option noImplicitAny hilft, diese Stellen zu entdecken.

Const Assertion

// Die per Typinferenz ermittelten Typen sind recht lose
let a = { foo: [1, 2, 3]};
// a: { foo: number[] }
 
// Mit "as const" werden die Typen so strikt wie möglich
let b = { foo: [1, 2, 3] } as const;
// b: { readony foo: readonly [1, 2, 3] }
Web

Type Alias

type Foo = number;
// "Foo" ist ein Alias für "number"
 
type Bar = { x: number };
// "Bar" ist ein neuer Objekttyp
 
type Baz = (arg: number) => number;
// "Baz" ist ein neuer Funktionstyp
Web

Merken!

Ein Typalias für einen Objekttyp ist unter fast allen Umständen das Gleiche wie ein Interface.

Declare-Keyword

// Typdefiniton
type Foo = { x: number };
// Gaukelt eine Variable "myFoo" vom Typ "Foo" vor
declare var myFoo: Foo;
// Typecheck OK, aber Laufzeitfehler
console.log(myFoo.x); // "myFoo" existiert nicht wirklich
 
// Typdefiniton (Funktionssignatur)
type Bar = (x: number) => number;
// Gaukelt eine Funktion "myFunc" vom Typ "Bar" vor
declare var myFunc: Bar;
// Typecheck OK, aber Laufzeitfehler
console.log(myFunc(1)); // "myFunc" existiert nicht wirklich
Web

WTF?

declare hat im Production-Code nichts zu suchen, ist aber sehr praktisch für theoretische Typexperimente! Es erspart uns das Instantiieren von komplexen Objekten und hält uns TypeScripts neunmalkluge Typinferenz vom Leib.

Interface

interface Foo { x: number }
// "Foo" ist ein neuer Objekttyp
 
interface Bar {
  (x: number): number;
}
// "Bar" ist ein neuer Funktionstyp
Web

Pro-Tipp!

Ein Interface mit mehr als einer Aufrufsyntax à la (x: number): number beschreibt den Typ einer überladenen Funktion. Klappt auch genau so mit Typalias.

Funktionsüberladung

// Zwei Aufrufsignaturen, eine Implementierungssignatur
function square(x: bigint): bigint; // Aufrufsignatur 1
function square(x: number): number; // Aufrufsignatur 2
function square(x: any): any { // Implementierungssignatur
  return x * x;
}
 
let s1 = square(2n);  // s1: bigint (Signatur 1)
let s2 = square(2);   // s2: number (Signatur 2)
let s3 = square("2"); // Ungültig
Web

Merken!

Funktionsüberladung ist das Mittel der Wahl, wenn sich für eine Funktion eine endlich lange Tabelle mit Signaturen (Parametern und Rückgabetypen) konstruieren lässt. Generics und Conditional Types sind für kompliziertere Fälle bestimmt.

Structural Subtyping

// "Foo" und "Bar" beschreiben die gleiche Objektstruktur und
// sind für TypeScript nicht unterscheidbar
type Foo = { x: number };
type Bar = { x: number };
 
declare var myFoo: Foo;
declare var myBar: Bar;
myFoo = myBar; // OK: "Foo" und "Bar" beschreiben das Gleiche
myBar = myFoo; // OK: "Foo" und "Bar" beschreiben das Gleiche
 
// "Baz" ergänzt die Struktur von "Foo/Bar" und ist daher ein
// Subtyp von "Foo/Bar". Daraus folgt: "Foo/Bar" beschreiben
// eine Teilstruktur von "Baz", sind daher Supertypen von "Baz".
type Baz = { x: number; y: string };
 
declare var myBaz: Baz;
myFoo = myBaz; // OK: "Baz" ist Subtyp von "Foo"
myBar = myBaz; // OK: "Baz" ist Subtyp von "Bar"
myBaz = myFoo; // Nicht OK: "Foo" ist Supertyp von "Baz"
myBaz = myBar; // Nicht OK: "Bar" ist Supertyp von "Baz"
Web

Merken!

TypeScripts Typsystem unterscheidet Typen nur anhand ihrer Fähigkeiten und Struktur (Structural Subtyping), nicht anhand ihrer Identität. Objekt-Typen mit gleichem Aufbau sind für das Typsystem nicht unterscheidbar. Subtypen können ihren Supertypen zugewiesen werden, umgekehrt nicht.

Declaration Merging

interface Foo { x: number } 
interface Foo { y: string }
// Typ "Foo" ist "{ x: number, y: string }"
// Multiple Interfaces im gleichen Scope werden // zusammengeführt
// Klappt nur mit Interfaces
Web

Readonly-Supertypen

// Normales Objekt mit normalem Array
type Foo = {
  values: string[];
}
 
// Readonly-Objekt mit Readonly-Array
type ReadonlyFoo = {
  readonly values: readonly string[];
}
 
// Normale Objekte sind Subtypen ihrer Readonly-Varianten,
// da sie im Vergleich mehr Funktionen bieten
declare var normalObj: Foo;
declare var readonlyObj: ReadonlyFoo;
 
normalObj.values.push("A"); // OK
readonlyObj.values.push("B"); // Nicht OK ("readonly")
 
readonlyObj = normalObj;
// OK: "normal" ist eine Erweiterung von "readonly"
normalObj = readonlyObj;
// Nicht OK: "readonly" fehlen Features von "normal"
Web

Merken!

Da eine Readonly-Variante eines gegebenen Typs T definitionsgemäß weniger Funktionen bietet als T selbst, ist ein Readonly-Typ von T immer ein Supertyp von T.

Tuple

// Typ für Arrays fester Länge mit definiertem Typ pro Index
type Foo = [number, string];
declare var myFoo: Foo;
 
myFoo = [1, "Hello"]; // OK
myFoo = ["Hello", 1]; // Nicht OK (falsche Typen pro Index)
myFoo = [1, "Hello", 2]; // Nicht OK ("Foo" ist nur zweistellig)
 
type At0 = Foo[0]; // At0 = number
type At1 = Foo[1]; // At0 = string
type At2 = Foo[2]; // Nicht OK ("Foo" ist nur zweistellig)
type L = Foo["length"]; // L = 2 (Literal Type)
Web

Generische Typen

type Foo<T, U> = (T | U)[];
// "Foo" baut aus den Typen "T" und "U" einen Arraytyp
type ArrayOfStringsAndNumbers = Foo<string, number>;
// = Ergibt "(string | number)[]"
 
interface Bar<T> { x: T }
// "Bar" verpackt einen Typ "T" in einem Objekttyp
type ObjectWithStringsForX = Bar<string>;
// = Ergibt "{ x: string }"
Web

Erkenntnis!

Sie können generische Typen als Typ-Factory-Funktionen verstehen. Es sind zwar technisch gesehen Typen. Doch entstehen benutzbare (instantiierbare) Typen erst, wenn sie mit Typparametern „aufgerufen“ werden. Zum Vergleich sind JavaScript-Funktionen auch technisch gesehen normale Objekte, aber ihr einziger Anwendungsfall ist, mit Parametern aufgerufen zu werden.

Generische Typen mit Defaults

type Foo<T = any> = { x: T };
// "...