// 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[] }
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.
// 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] }
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
Ein Typalias für einen Objekttyp ist unter fast allen Umständen das Gleiche wie ein Interface.
// 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
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 Foo { x: number }
// "Foo" ist ein neuer Objekttyp
interface Bar {
(x: number): number;
}
// "Bar" ist ein neuer Funktionstyp
Ein Interface mit mehr als einer Aufrufsyntax à la (x: number): number beschreibt den Typ einer überladenen Funktion. Klappt auch genau so mit Typalias.
// 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
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.
// "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"
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.
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
// 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"
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.
// 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)
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 }"
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.
type Foo<T = any> = { x: T };
// "...