Object vs. object in TypeScriptObjectObject (Großbuchstabe „O“) in TypeScript: Instanzen der Klasse Objectobject (Kleinbuchstabe „o“) in TypeScript: Nicht-primitive WerteObject vs. object: Primitive WerteObject vs. object: Inkompatible EigenschaftstypenObjectIn diesem Kapitel untersuchen wir, wie Objekte und Eigenschaften in TypeScript statisch typisiert werden.
In JavaScript können Objekte zwei Rollen spielen (immer mindestens eine davon, manchmal auch gemischt)
Records haben eine feste Anzahl von Eigenschaften, die zur Entwicklungszeit bekannt sind. Jede Eigenschaft kann einen anderen Typ haben.
Dictionaries haben eine beliebige Anzahl von Eigenschaften, deren Namen zur Entwicklungszeit nicht bekannt sind. Alle Eigenschaftsschlüssel (Strings und/oder Symbole) haben denselben Typ, ebenso wie die Eigenschaftswerte.
Zuerst und vor allem werden wir Objekte als Records untersuchen. Objekte als Dictionaries werden wir später in diesem Kapitel kurz behandeln.
Es gibt zwei verschiedene allgemeine Typen für Objekte
Object mit großem „O“ ist der Typ aller Instanzen der Klasse Object
let obj1: Object;object mit kleinem „o“ ist der Typ aller nicht-primitiven Werte
let obj2: object;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.
Object vs. object in TypeScriptObjectIn Plain JavaScript gibt es einen wichtigen Unterschied.
Einerseits sind die meisten Objekte Instanzen von Object.
> const obj1 = {};
> obj1 instanceof Object
trueDas bedeutet
Object.prototype befindet sich in ihren Prototypketten
> Object.prototype.isPrototypeOf(obj1)
trueSie erben seine Eigenschaften.
> obj1.toString === Object.prototype.toString
trueAndererseits 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)
nullobj2 ist ein Objekt, das keine Instanz der Klasse Object ist
> typeof obj2
'object'
> obj2 instanceof Object
falseObject (Großbuchstabe „O“) in TypeScript: Instanzen der Klasse ObjectZur Erinnerung: Jede Klasse C erstellt zwei Entitäten
C.C, die Instanzen der Konstruktorfunktion beschreibt.Ähnlich hat TypeScript zwei eingebaute Schnittstellen
Die Schnittstelle Object gibt die Eigenschaften von Instanzen von Object an, einschließlich der von Object.prototype geerbten Eigenschaften.
Die Schnittstelle ObjectConstructor gibt die Eigenschaften der Klasse Object an.
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
Object (Zeile C) als auch einen Typ namens Object (Zeile A).Object haben keine eigenen Eigenschaften, daher passt Object.prototype ebenfalls zu Object (Zeile B).object (Kleinbuchstabe „o“) in TypeScript: Nicht-primitive WerteIn 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.
Object vs. object: Primitive WerteInteressanterweise passt auch der Typ Object zu primitiven Werten
function func1(x: Object) { }
func1('abc'); // OKWarum ist das so? Primitive Werte haben alle Eigenschaften, die von Object benötigt werden, da sie Object.prototype erben
> 'abc'.hasOwnProperty === Object.prototype.hasOwnProperty
trueUmgekehrt 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');Object vs. object: Inkompatible EigenschaftstypenMit 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 } };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.
In diesem Abschnitt betrachten wir die wichtigsten Unterschiede zwischen Objekttyp-Literalen und Schnittstellen.
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;
}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',
};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.
this-TypenPolymorphe 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).
Schnittstellen arbeiten strukturell – sie müssen nicht implementiert werden, um übereinzustimmen
interface Point {
x: number;
y: number;
}
const point: Point = {x: 1, y: 2}; // OKWeitere Informationen zu diesem Thema finden Sie in [Inhalt nicht enthalten].
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 definieren Eigenschaften
myProperty: boolean;Methodensignaturen definieren Methoden
myMethod(str: string): number;Hinweis: Die Namen von Parametern (in diesem Fall: str) dienen der Dokumentation, wie Dinge funktionieren, haben aber keinen anderen Zweck.
Indexsignaturen werden benötigt, um Arrays oder Objekte zu beschreiben, die als Dictionaries verwendet werden.
[key: string]: any;Hinweis: Der Name key dient nur zu Dokumentationszwecken.
Aufrufsignaturen ermöglichen es Schnittstellen, Funktionen zu beschreiben
(num: number): string;Konstruktor-Signaturen ermöglichen es Schnittstellen, Klassen und Konstruktorfunktionen zu beschreiben
new(str: string): ExampleInstance; 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.
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.
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');Indexsignaturschlüssel müssen entweder string oder number sein
any ist nicht erlaubt.string|number) sind nicht erlaubt. Allerdings können pro Schnittstelle mehrere Indexsignaturen verwendet werden.Ä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] };
}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;
}ObjectAlle 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;
}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
.x und .y mit den angegebenen Typen haben. Mit anderen Worten: Diese Objekte dürfen keine *überschüssigen Eigenschaften* haben (mehr als die erforderlichen Eigenschaften)..x und .y haben. Mit anderen Worten: Überschüssige Eigenschaften sind erlaubt.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); // OKWenn 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}); // OKWarum 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'});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.
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}; // OKWenn 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 = {}; // OKWas, 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); // OKEine 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 }); // OKEine 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 }); // OKWir machen weiter mit zwei Beispielen, bei denen TypeScript das Nicht-Zulassen von überschüssigen Eigenschaften als Problem darstellt.
IncrementorIn 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;
}.dateStrDie 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.
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.
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'};undefined|stringWas 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.
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;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.