TypeScript meistern
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

15 Objekttypen



In diesem Kapitel untersuchen wir, wie Objekte und Eigenschaften in TypeScript statisch typisiert werden.

15.1 Rollen von Objekten

In JavaScript können Objekte zwei Rollen spielen (immer mindestens eine davon, manchmal auch gemischt)

Zuerst und vor allem werden wir Objekte als Records untersuchen. Objekte als Dictionaries werden wir später in diesem Kapitel kurz behandeln.

15.2 Typen für Objekte

Es gibt zwei verschiedene allgemeine Typen für Objekte

Objekte können auch über ihre Eigenschaften typisiert werden

// Object type literal
let obj3: {prop: boolean};

// Interface
interface ObjectType {
  prop: boolean;
}
let obj4: ObjectType;

In den nächsten Abschnitten werden wir all diese Möglichkeiten, Objekte zu typisieren, detaillierter untersuchen.

15.3 Object vs. object in TypeScript

15.3.1 Plain JavaScript: Objekte vs. Instanzen von Object

In Plain JavaScript gibt es einen wichtigen Unterschied.

Einerseits sind die meisten Objekte Instanzen von Object.

> const obj1 = {};
> obj1 instanceof Object
true

Das bedeutet

Andererseits können wir auch Objekte erstellen, die Object.prototype nicht in ihrer Prototypkette haben. Zum Beispiel hat das folgende Objekt überhaupt keinen Prototyp

> const obj2 = Object.create(null);
> Object.getPrototypeOf(obj2)
null

obj2 ist ein Objekt, das keine Instanz der Klasse Object ist

> typeof obj2
'object'
> obj2 instanceof Object
false

15.3.2 Object (Großbuchstabe „O“) in TypeScript: Instanzen der Klasse Object

Zur Erinnerung: Jede Klasse C erstellt zwei Entitäten

Ähnlich hat TypeScript zwei eingebaute Schnittstellen

Dies sind die Schnittstellen

interface Object { // (A)
  constructor: Function;
  toString(): string;
  toLocaleString(): string;
  valueOf(): Object;
  hasOwnProperty(v: PropertyKey): boolean;
  isPrototypeOf(v: Object): boolean;
  propertyIsEnumerable(v: PropertyKey): boolean;
}

interface ObjectConstructor {
  /** Invocation via `new` */
  new(value?: any): Object;
  /** Invocation via function calls */
  (value?: any): any;

  readonly prototype: Object; // (B)

  getPrototypeOf(o: any): any;

  // ···
}
declare var Object: ObjectConstructor; // (C)

Beobachtungen

15.3.3 object (Kleinbuchstabe „o“) in TypeScript: Nicht-primitive Werte

In TypeScript ist object der Typ aller nicht-primitiven Werte (primitive Werte sind undefined, null, Booleans, Zahlen, Bigints, Strings). Mit diesem Typ können wir nicht auf Eigenschaften eines Wertes zugreifen.

15.3.4 Object vs. object: Primitive Werte

Interessanterweise passt auch der Typ Object zu primitiven Werten

function func1(x: Object) { }
func1('abc'); // OK

Warum ist das so? Primitive Werte haben alle Eigenschaften, die von Object benötigt werden, da sie Object.prototype erben

> 'abc'.hasOwnProperty === Object.prototype.hasOwnProperty
true

Umgekehrt passt object nicht zu primitiven Werten

function func2(x: object) { }
// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'object'. (2345)
func2('abc');

15.3.5 Object vs. object: Inkompatible Eigenschaftstypen

Mit dem Typ Object beschwert sich TypeScript, wenn ein Objekt eine Eigenschaft hat, deren Typ mit der entsprechenden Eigenschaft in der Schnittstelle Object kollidiert

// @ts-expect-error: Type '() => number' is not assignable to
// type '() => string'.
//   Type 'number' is not assignable to type 'string'. (2322)
const obj1: Object = { toString() { return 123 } };

Mit dem Typ object beschwert sich TypeScript nicht (da object keine Eigenschaften angibt und es keine Konflikte geben kann)

const obj2: object = { toString() { return 123 } };

15.4 Objekttyp-Literale und Schnittstellen

TypeScript bietet zwei sehr ähnliche Möglichkeiten, Objekttypen zu definieren

// Object type literal
type ObjType1 = {
  a: boolean,
  b: number;
  c: string,
};

// Interface
interface ObjType2 {
  a: boolean,
  b: number;
  c: string,
}

Wir können entweder Semikolons oder Kommas als Trennzeichen verwenden. Nachgestellte Trennzeichen sind erlaubt und optional.

15.4.1 Unterschiede zwischen Objekttyp-Literalen und Schnittstellen

In diesem Abschnitt betrachten wir die wichtigsten Unterschiede zwischen Objekttyp-Literalen und Schnittstellen.

15.4.1.1 Inline-Definition

Objekttyp-Literale können inline definiert werden, Schnittstellen jedoch nicht

// Inlined object type literal:
function f1(x: {prop: number}) {}

// Referenced interface:
function f2(x: ObjectInterface) {} 
interface ObjectInterface {
  prop: number;
}
15.4.1.2 Doppelte Namen

Typaliase mit doppelten Namen sind ungültig

// @ts-expect-error: Duplicate identifier 'PersonAlias'. (2300)
type PersonAlias = {first: string};
// @ts-expect-error: Duplicate identifier 'PersonAlias'. (2300)
type PersonAlias = {last: string};

Umgekehrt werden Schnittstellen mit doppelten Namen zusammengeführt

interface PersonInterface {
  first: string;
}
interface PersonInterface {
  last: string;
}
const jane: PersonInterface = {
  first: 'Jane',
  last: 'Doe',
};
15.4.1.3 Gemappte Typen

Für gemappte Typen (Zeile A) müssen wir Objekttyp-Literale verwenden

interface Point {
  x: number;
  y: number;
}

type PointCopy1 = {
  [Key in keyof Point]: Point[Key]; // (A)
};

// Syntax error:
// interface PointCopy2 {
//   [Key in keyof Point]: Point[Key];
// };

  Mehr Informationen zu gemappten Typen

Gemappte Typen liegen außerhalb des aktuellen Umfangs dieses Buches. Weitere Informationen finden Sie in der TypeScript-Dokumentation.

15.4.1.4 Polymorphe this-Typen

Polymorphe this-Typen können nur in Schnittstellen verwendet werden

interface AddsStrings {
  add(str: string): this;
};

class StringBuilder implements AddsStrings {
  result = '';
  add(str: string) {
    this.result += str;
    return this;
  }
}

  Quelle dieses Abschnitts

  Ab jetzt bedeutet „Schnittstelle“ „Schnittstelle oder Objekttyp-Literal“ (sofern nicht anders angegeben).

15.4.2 Schnittstellen arbeiten strukturell in TypeScript

Schnittstellen arbeiten strukturell – sie müssen nicht implementiert werden, um übereinzustimmen

interface Point {
  x: number;
  y: number;
}
const point: Point = {x: 1, y: 2}; // OK

Weitere Informationen zu diesem Thema finden Sie in [Inhalt nicht enthalten].

15.4.3 Mitglieder von Schnittstellen und Objekttyp-Literalen

Die Konstrukte innerhalb der Körper von Schnittstellen und Objekttyp-Literalen werden als deren Mitglieder bezeichnet. Dies sind die gängigsten Mitglieder

interface ExampleInterface {
  // Property signature
  myProperty: boolean;

  // Method signature
  myMethod(str: string): number;

  // Index signature
  [key: string]: any;

  // Call signature
  (num: number): string;

  // Construct signature
  new(str: string): ExampleInstance; 
}
interface ExampleInstance {}

Schauen wir uns diese Mitglieder genauer an

Eigenschaftssignaturen sollten selbsterklärend sein. Aufrufsignaturen und Konstruktor-Signaturen werden später in diesem Buch beschrieben. Methodensignaturen und Indexsignaturen werden wir als Nächstes genauer betrachten.

15.4.4 Methodensignaturen

Aus Sicht des Typsystems von TypeScript sind Methodendefinitionen und Eigenschaften, deren Werte Funktionen sind, gleichwertig

interface HasMethodDef {
  simpleMethod(flag: boolean): void;
}
interface HasFuncProp {
  simpleMethod: (flag: boolean) => void;
}

const objWithMethod: HasMethodDef = {
  simpleMethod(flag: boolean): void {},
};
const objWithMethod2: HasFuncProp = objWithMethod;

const objWithOrdinaryFunction: HasMethodDef = {
  simpleMethod: function (flag: boolean): void {},
};
const objWithOrdinaryFunction2: HasFuncProp = objWithOrdinaryFunction;

const objWithArrowFunction: HasMethodDef = {
  simpleMethod: (flag: boolean): void => {},
};
const objWithArrowFunction2: HasFuncProp = objWithArrowFunction;

Meine Empfehlung ist, die Syntax zu verwenden, die am besten ausdrückt, wie eine Eigenschaft eingerichtet werden soll.

15.4.5 Indexsignaturen: Objekte als Dictionaries

Bisher haben wir Schnittstellen nur für Objekte-als-Records mit festen Schlüsseln verwendet. Wie drücken wir aus, dass ein Objekt als Dictionary verwendet werden soll? Zum Beispiel: Was soll TranslationDict im folgenden Codefragment sein?

function translate(dict: TranslationDict, english: string): string {
  return dict[english];
}

Wir verwenden eine Indexsignatur (Zeile A), um auszudrücken, dass TranslationDict für Objekte ist, die String-Schlüssel auf String-Werte abbilden

interface TranslationDict {
  [key:string]: string; // (A)
}
const dict = {
  'yes': 'sí',
  'no': 'no',
  'maybe': 'tal vez',
};
assert.equal(
  translate(dict, 'maybe'),
  'tal vez');
15.4.5.1 Typisierung von Indexsignaturschlüsseln

Indexsignaturschlüssel müssen entweder string oder number sein

15.4.5.2 String-Schlüssel vs. Nummernschlüssel

Ähnlich wie in Plain JavaScript sind die Nummerneigenschaftsschlüssel von TypeScript eine Teilmenge der String-Eigenschaftsschlüssel (siehe „JavaScript für ungeduldige Programmierer“). Daher muss, wenn wir sowohl eine String- als auch eine Nummer-Indexsignatur haben, der Eigenschaftstyp des ersteren ein Supertyp des letzteren sein. Das folgende Beispiel funktioniert, da Object ein Supertyp von RegExp ist

interface StringAndNumberKeys {
  [key: string]: Object;
  [key: number]: RegExp;
}

// %inferred-type: (x: StringAndNumberKeys) =>
// { str: Object; num: RegExp; }
function f(x: StringAndNumberKeys) {
  return { str: x['abc'], num: x[123] };
}
15.4.5.3 Indexsignaturen vs. Eigenschafts- und Methodensignaturen

Wenn eine Schnittstelle sowohl eine Indexsignatur als auch Eigenschaften- und/oder Methodensignaturen enthält, muss der Typ der Index-Eigenschaftswerts ebenfalls ein Supertyp des Typs des Eigenschaften- und/oder Methodenwerts sein.

interface I1 {
  [key: string]: boolean;

  // @ts-expect-error: Property 'myProp' of type 'number' is not assignable
  // to string index type 'boolean'. (2411)
  myProp: number;
  
  // @ts-expect-error: Property 'myMethod' of type '() => string' is not
  // assignable to string index type 'boolean'. (2411)
  myMethod(): string;
}

Im Gegensatz dazu erzeugen die folgenden beiden Schnittstellen keine Fehler

interface I2 {
  [key: string]: number;
  myProp: number;
}

interface I3 {
  [key: string]: () => string;
  myMethod(): string;
}

15.4.6 Schnittstellen beschreiben Instanzen von Object

Alle Schnittstellen beschreiben Objekte, die Instanzen von Object sind und die Eigenschaften von Object.prototype erben.

Im folgenden Beispiel ist der Parameter x vom Typ {} mit dem Rückgabetyp Object kompatibel

function f1(x: {}): Object {
  return x;
}

Ähnlich hat {} eine Methode .toString()

function f2(x: {}): { toString(): string } {
  return x;
}

15.4.7 Prüfung auf überschüssige Eigenschaften: Wann sind zusätzliche Eigenschaften erlaubt?

Betrachten Sie als Beispiel die folgende Schnittstelle

interface Point {
  x: number;
  y: number;
}

Es gibt (unter anderem) zwei Möglichkeiten, wie diese Schnittstelle interpretiert werden könnte

TypeScript verwendet beide Interpretationen. Um zu untersuchen, wie das funktioniert, verwenden wir die folgende Funktion

function computeDistance(point: Point) { /*...*/ }

Standardmäßig ist die überschüssige Eigenschaft .z erlaubt

const obj = { x: 1, y: 2, z: 3 };
computeDistance(obj); // OK

Wenn wir jedoch direkt Objektliterale verwenden, sind überschüssige Eigenschaften verboten

// @ts-expect-error: Argument of type '{ x: number; y: number; z: number; }'
// is not assignable to parameter of type 'Point'.
//   Object literal may only specify known properties, and 'z' does not
//   exist in type 'Point'. (2345)
computeDistance({ x: 1, y: 2, z: 3 }); // error

computeDistance({x: 1, y: 2}); // OK
15.4.7.1 Warum sind überschüssige Eigenschaften in Objektliteralen verboten?

Warum die strengeren Regeln für Objektliterale? Sie bieten Schutz vor Tippfehlern bei Eigenschaftsschlüsseln. Wir verwenden die folgende Schnittstelle, um zu demonstrieren, was das bedeutet.

interface Person {
  first: string;
  middle?: string;
  last: string;
}
function computeFullName(person: Person) { /*...*/ }

Die Eigenschaft .middle ist optional und kann weggelassen werden (optionale Eigenschaften werden später in diesem Kapitel behandelt). Für TypeScript sieht das Vertippen ihres Namens wie das Weglassen aus und das Hinzufügen einer überschüssigen Eigenschaft. Dennoch wird der Tippfehler erkannt, da überschüssige Eigenschaften in diesem Fall nicht erlaubt sind

// @ts-expect-error: Argument of type '{ first: string; mdidle: string;
// last: string; }' is not assignable to parameter of type 'Person'.
//   Object literal may only specify known properties, but 'mdidle'
//   does not exist in type 'Person'. Did you mean to write 'middle'?
computeFullName({first: 'Jane', mdidle: 'Cecily', last: 'Doe'});
15.4.7.2 Warum sind überschüssige Eigenschaften erlaubt, wenn ein Objekt von woandersher kommt?

Die Idee ist, dass, wenn ein Objekt von woandersher kommt, wir davon ausgehen können, dass es bereits überprüft wurde und keine Tippfehler enthält. Dann können wir es uns leisten, weniger vorsichtig zu sein.

Wenn Tippfehler keine Rolle spielen, sollte unser Ziel die Maximierung der Flexibilität sein. Betrachten Sie die folgende Funktion

interface HasYear {
  year: number;
}

function getAge(obj: HasYear) {
  const yearNow = new Date().getFullYear();
  return yearNow - obj.year;
}

Ohne überschüssige Eigenschaften für die meisten an getAge() übergebenen Werte zuzulassen, wäre der Nutzen dieser Funktion recht begrenzt.

15.4.7.3 Leere Schnittstellen erlauben überschüssige Eigenschaften

Wenn eine Schnittstelle leer ist (oder das Objekttyp-Literal {} verwendet wird), sind überschüssige Eigenschaften immer erlaubt

interface Empty { }
interface OneProp {
  myProp: number;
}

// @ts-expect-error: Type '{ myProp: number; anotherProp: number; }' is not
// assignable to type 'OneProp'.
//   Object literal may only specify known properties, and
//   'anotherProp' does not exist in type 'OneProp'. (2322)
const a: OneProp = { myProp: 1, anotherProp: 2 };
const b: Empty = {myProp: 1, anotherProp: 2}; // OK
15.4.7.4 Nur Objekte ohne Eigenschaften abgleichen

Wenn wir erzwingen wollen, dass ein Objekt keine Eigenschaften hat, können wir den folgenden Trick anwenden (Quelle: Geoff Goodman)

interface WithoutProperties {
  [key: string]: never;
}

// @ts-expect-error: Type 'number' is not assignable to type 'never'. (2322)
const a: WithoutProperties = { prop: 1 };
const b: WithoutProperties = {}; // OK
15.4.7.5 Überschüssige Eigenschaften in Objektliteralen erlauben

Was, wenn wir überschüssige Eigenschaften in Objektliteralen erlauben wollen? Betrachten Sie als Beispiel die Schnittstelle Point und die Funktion computeDistance1()

interface Point {
  x: number;
  y: number;
}

function computeDistance1(point: Point) { /*...*/ }

// @ts-expect-error: Argument of type '{ x: number; y: number; z: number; }'
// is not assignable to parameter of type 'Point'.
//   Object literal may only specify known properties, and 'z' does not
//   exist in type 'Point'. (2345)
computeDistance1({ x: 1, y: 2, z: 3 });

Eine Option ist, das Objektliteral einer temporären Variablen zuzuweisen

const obj = { x: 1, y: 2, z: 3 };
computeDistance1(obj);

Eine zweite Option ist die Verwendung einer Typassertion

computeDistance1({ x: 1, y: 2, z: 3 } as Point); // OK

Eine dritte Option ist, computeDistance1() neu zu schreiben, damit sie einen Typparameter verwendet

function computeDistance2<P extends Point>(point: P) { /*...*/ }
computeDistance2({ x: 1, y: 2, z: 3 }); // OK

Eine vierte Option ist, die Schnittstelle Point zu erweitern, damit sie überschüssige Eigenschaften erlaubt

interface PointEtc extends Point {
  [key: string]: any;
}
function computeDistance3(point: PointEtc) { /*...*/ }

computeDistance3({ x: 1, y: 2, z: 3 }); // OK

Wir machen weiter mit zwei Beispielen, bei denen TypeScript das Nicht-Zulassen von überschüssigen Eigenschaften als Problem darstellt.

15.4.7.5.1 Überschüssige Eigenschaften erlauben: Beispiel Incrementor

In diesem Beispiel möchten wir einen Incrementor implementieren, aber TypeScript erlaubt die zusätzliche Eigenschaft .counter nicht

interface Incrementor {
  inc(): void
}
function createIncrementor(start = 0): Incrementor {
  return {
    // @ts-expect-error: Type '{ counter: number; inc(): void; }' is not
    // assignable to type 'Incrementor'.
    //   Object literal may only specify known properties, and
    //   'counter' does not exist in type 'Incrementor'. (2322)
    counter: start,
    inc() {
      // @ts-expect-error: Property 'counter' does not exist on type
      // 'Incrementor'. (2339)
      this.counter++;
    },
  };
}

Leider gibt es selbst mit einer Typassertion noch einen Typfehler

function createIncrementor2(start = 0): Incrementor {
  return {
    counter: start,
    inc() {
      // @ts-expect-error: Property 'counter' does not exist on type
      // 'Incrementor'. (2339)
      this.counter++;
    },
  } as Incrementor;
}

Wir können entweder eine Indexsignatur zur Schnittstelle Incrementor hinzufügen. Oder – insbesondere wenn das nicht möglich ist – eine temporäre Variable einführen

function createIncrementor3(start = 0): Incrementor {
  const incrementor = {
    counter: start,
    inc() {
      this.counter++;
    },
  };
  return incrementor;
}
15.4.7.5.2 Überschüssige Eigenschaften erlauben: Beispiel .dateStr

Die folgende Vergleichsfunktion kann verwendet werden, um Objekte zu sortieren, die die Eigenschaft .dateStr haben

function compareDateStrings(
  a: {dateStr: string}, b: {dateStr: string}) {
    if (a.dateStr < b.dateStr) {
      return +1;
    } else if (a.dateStr > b.dateStr) {
      return -1;
    } else {
      return 0;
    }
  }

Zum Beispiel in Unit-Tests möchten wir diese Funktion möglicherweise direkt mit Objektliteralen aufrufen. TypeScript erlaubt uns dies nicht, und wir müssen einen der Workarounds verwenden.

15.5 Typinferenz

Dies sind die Typen, die TypeScript für Objekte ableitet, die auf verschiedene Weise erstellt werden

// %inferred-type: Object
const obj1 = new Object();

// %inferred-type: any
const obj2 = Object.create(null);

// %inferred-type: {}
const obj3 = {};

// %inferred-type: { prop: number; }
const obj4 = {prop: 123};

// %inferred-type: object
const obj5 = Reflect.getPrototypeOf({});

Grundsätzlich könnte der Rückgabetyp von Object.create() object sein. any erlaubt uns jedoch, dem Ergebnis Eigenschaften hinzuzufügen und es zu ändern.

15.6 Weitere Funktionen von Schnittstellen

15.6.1 Optionale Eigenschaften

Wenn wir ein Fragezeichen (?) nach dem Namen einer Eigenschaft setzen, ist diese Eigenschaft optional. Die gleiche Syntax wird verwendet, um Parameter von Funktionen, Methoden und Konstruktoren als optional zu markieren. Im folgenden Beispiel ist die Eigenschaft .middle optional

interface Name {
  first: string;
  middle?: string;
  last: string;
}

Daher ist es in Ordnung, diese Eigenschaft wegzulassen (Zeile A)

const john: Name = {first: 'Doe', last: 'Doe'}; // (A)
const jane: Name = {first: 'Jane', middle: 'Cecily', last: 'Doe'};
15.6.1.1 Optional vs. undefined|string

Was ist der Unterschied zwischen .prop1 und .prop2?

interface Interf {
  prop1?: string;
  prop2: undefined | string; 
}

Eine optionale Eigenschaft kann alles, was undefined|string kann. Wir können sogar den Wert undefined für erstere verwenden

const obj1: Interf = { prop1: undefined, prop2: undefined };

Allerdings kann nur .prop1 weggelassen werden

const obj2: Interf = { prop2: undefined };

// @ts-expect-error: Property 'prop2' is missing in type '{}' but required
// in type 'Interf'. (2741)
const obj3: Interf = { };

Typen wie undefined|string und null|string sind nützlich, wenn wir Weglassungen explizit machen wollen. Wenn Leute eine solche explizit weggelassene Eigenschaft sehen, wissen sie, dass sie existiert, aber ausgeschaltet wurde.

15.6.2 Schreibgeschützte Eigenschaften

Im folgenden Beispiel ist die Eigenschaft .prop schreibgeschützt

interface MyInterface {
  readonly prop: number;
}

Infolgedessen können wir sie lesen, aber nicht ändern

const obj: MyInterface = {
  prop: 1,
};

console.log(obj.prop); // OK

// @ts-expect-error: Cannot assign to 'prop' because it is a read-only
// property. (2540)
obj.prop = 2;

15.7 JavaScript-Prototypketten und TypeScript-Typen

TypeScript unterscheidet nicht zwischen eigenen und geerbten Eigenschaften. Sie werden alle einfach als Eigenschaften betrachtet.

interface MyInterface {
  toString(): string; // inherited property
  prop: number; // own property
}
const obj: MyInterface = { // OK
  prop: 123,
};

obj erbt .toString() von Object.prototype.

Der Nachteil dieses Ansatzes ist, dass einige Phänomene in JavaScript nicht über das Typsystem von TypeScript beschrieben werden können. Der Vorteil ist, dass das Typsystem einfacher ist.

15.8 Quellen dieses Kapitels