Object
與 object
Object
的實例Object
(大寫「O」):Object
類別的實例object
(小寫「o」):非原始值Object
與 object
:原始值Object
與 object
:不相容的屬性類型Object
的實例在本章中,我們將探討物件和屬性如何在 TypeScript 中以靜態方式鍵入。
在 JavaScript 中,物件可以扮演兩個角色(總是至少扮演其中一個,有時是混合)
記錄在開發時間已知,具有固定數量的屬性。每個屬性可以有不同的類型。
字典具有任意數量的屬性,其名稱在開發時間未知。所有屬性鍵(字串和/或符號)具有相同的類型,屬性值也是如此。
首先,我們將探討作為記錄的物件。稍後在本章中,我們將簡要介紹作為字典的物件 。
物件有兩種不同的類型
Object
,大寫「O」,是 Object
類別所有實例的類型
: Object; let obj1
object
,小寫「o」,是非原始值的類型
: object; let obj2
物件也可以透過其屬性來設定類型
// Object type literal
: {prop: boolean};
let obj3
// Interface
interface ObjectType {: boolean;
prop
}: ObjectType; let obj4
在下一節中,我們將更詳細地探討所有這些物件類型設定方式。
Object
與 object
Object
的實例在純 JavaScript 中,有一個重要的區別。
一方面,大多數物件都是 Object
的實例。
> const obj1 = {};
> obj1 instanceof Objecttrue
這表示
Object.prototype
在其原型鏈中
> Object.prototype.isPrototypeOf(obj1)true
它們繼承其屬性。
> obj1.toString === Object.prototype.toStringtrue
另一方面,我們也可以建立沒有 Object.prototype
在其原型鏈中的物件。例如,下列物件完全沒有任何原型
> const obj2 = Object.create(null);
> Object.getPrototypeOf(obj2)null
obj2
是不是 Object
類別實例的物件
> typeof obj2'object'
> obj2 instanceof Objectfalse
Object
(大寫「O」):Object
類別的實例回想一下每個類別 C
會建立兩個實體
C
。C
,用來描述建構函式的實例。類似地,TypeScript 有兩個內建介面
介面 Object
指定 Object
實例的屬性,包括從 Object.prototype
繼承的屬性。
介面 ObjectConstructor
指定 Object
類別的屬性。
這些是介面
// (A)
interface Object { : Function;
constructortoString(): 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 */
?: any): any;
(value
readonly prototype: Object; // (B)
getPrototypeOf(o: any): any;
// ···
}declare var Object: ObjectConstructor; // (C)
觀察
Object
(C 行)和一個類型名稱為 Object
(A 行)。Object
的直接實例沒有自己的屬性,因此 Object.prototype
也符合 Object
(B 行)。object
(小寫「o」):非原始值在 TypeScript 中,object
是所有非原始值(原始值為 undefined
、null
、布林值、數字、大整數、字串)的類型。有了這個類型,我們無法存取任何值的屬性。
Object
與 object
:原始值有趣的是,類型 Object
也符合原始值
function func1(x: Object) { }
func1('abc'); // OK
為什麼會這樣?原始值具有 Object
所需的所有屬性,因為它們繼承了 Object.prototype
> 'abc'.hasOwnProperty === Object.prototype.hasOwnPropertytrue
相反地,object
並不符合原始值
function func2(x: object) { }
// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'object'. (2345)
func2('abc');
Object
與 object
:不相容的屬性類型使用類型 Object
時,如果物件具有與介面 Object
中對應屬性衝突的屬性,TypeScript 會抱怨
// @ts-expect-error: Type '() => number' is not assignable to
// type '() => string'.
// Type 'number' is not assignable to type 'string'. (2322)
: Object = { toString() { return 123 } }; const obj1
使用類型 object
時,TypeScript 不會抱怨(因為 object
沒有指定任何屬性,因此不會有任何衝突)
: object = { toString() { return 123 } }; const obj2
TypeScript 有兩種定義物件類型的方式,非常類似
// Object type literal
type ObjType1 = {
: boolean,
a: number;
b: string,
c;
}
// Interface
interface ObjType2 {: boolean,
a: number;
b: string,
c }
我們可以使用分號或逗號作為分隔符號。允許並選擇尾隨分隔符號。
在本節中,我們將探討物件類型文字和介面之間最重要的差異。
物件類型文字可以內聯,而介面則不能
// Inlined object type literal:
function f1(x: {prop: number}) {}
// Referenced interface:
function f2(x: ObjectInterface) {}
interface ObjectInterface {: number;
prop }
具有重複名稱的類型別名是非法的
// @ts-expect-error: Duplicate identifier 'PersonAlias'. (2300)
type PersonAlias = {first: string};
// @ts-expect-error: Duplicate identifier 'PersonAlias'. (2300)
type PersonAlias = {last: string};
相反地,具有重複名稱的介面會合併
interface PersonInterface {: string;
first
}
interface PersonInterface {: string;
last
}: PersonInterface = {
const jane: 'Jane',
first: 'Doe',
last; }
對於對應類型(第 A 行),我們需要使用物件類型文字
interface Point {: number;
x: number;
y
}
type PointCopy1 = {
keyof Point]: Point[Key]; // (A)
[Key in ;
}
// Syntax error:
// interface PointCopy2 {
// [Key in keyof Point]: Point[Key];
// };
有關對應類型的更多資訊
對應類型超出了本書的當前範圍。如需更多資訊,請參閱 TypeScript 手冊。
this
類型多型 this
類型只能用於介面
interface AddsStrings {add(str: string): this;
;
}
class StringBuilder implements AddsStrings {= '';
result add(str: string) {
.result += str;
this;
return this
} }
從現在開始,「介面」表示「介面或物件類型文字」(除非另有說明)。
介面以結構方式運作,它們不必實作即可匹配
interface Point {: number;
x: number;
y
}: Point = {x: 1, y: 2}; // OK const point
有關此主題的更多資訊,請參閱 [未包含的內容]。
介面和物件類型文字主體內的建構稱為其成員。以下是常見的成員
interface ExampleInterface {// Property signature
: boolean;
myProperty
// Method signature
myMethod(str: string): number;
// Index signature
: string]: any;
[key
// Call signature
: number): string;
(num
// Construct signature
new(str: string): ExampleInstance;
} interface ExampleInstance {}
讓我們更詳細地檢視這些成員
屬性簽章定義屬性
: boolean; myProperty
方法簽章定義方法
myMethod(str: string): number;
注意:參數的名稱(在此情況下:str
)有助於記錄運作方式,但沒有其他用途。
索引簽章需要用來描述用作字典的陣列或物件。
: string]: any; [key
注意:名稱 key
僅用於文件目的。
呼叫簽章讓介面能夠描述函式
: number): string; (num
建構簽章讓介面能夠描述類別和建構函式
new(str: string): ExampleInstance;
屬性簽章應該是自明。 呼叫簽章 和 建構簽章 會在本書稍後描述。接下來,我們將更仔細地檢視方法簽章和索引簽章。
就 TypeScript 的類型系統而言,方法定義和值為函式的屬性是等效的
interface HasMethodDef {simpleMethod(flag: boolean): void;
}
interface HasFuncProp {: (flag: boolean) => void;
simpleMethod
}
: HasMethodDef = {
const objWithMethodsimpleMethod(flag: boolean): void {},
;
}: HasFuncProp = objWithMethod;
const objWithMethod2
: HasMethodDef = {
const objWithOrdinaryFunction: function (flag: boolean): void {},
simpleMethod;
}: HasFuncProp = objWithOrdinaryFunction;
const objWithOrdinaryFunction2
: HasMethodDef = {
const objWithArrowFunction: (flag: boolean): void => {},
simpleMethod;
}: HasFuncProp = objWithArrowFunction; const objWithArrowFunction2
我的建議是使用最能表達如何設定屬性的語法。
到目前為止,我們只將介面用於具有固定金鑰的物件作為記錄。我們如何表達物件要作為字典使用的事實?例如:在下列程式碼片段中,TranslationDict
應該是什麼?
function translate(dict: TranslationDict, english: string): string {
;
return dict[english] }
我們使用索引簽章(A 行)來表達 TranslationDict
是用於將字串金鑰對應到字串值的物件
interface TranslationDict {:string]: string; // (A)
[key
}= {
const dict 'yes': 'sí',
'no': 'no',
'maybe': 'tal vez',
;
}.equal(
asserttranslate(dict, 'maybe'),
'tal vez');
索引簽章金鑰必須是 string
或 number
any
。string|number
)。但是,每個介面可以使用多個索引簽章。就像在純 JavaScript 中一樣,TypeScript 的數字屬性金鑰是字串屬性金鑰的子集(請參閱「給急躁的程式設計師的 JavaScript」)。因此,如果我們同時有字串索引簽章和數字索引簽章,前者的屬性類型必須是後者的超類型。下列範例之所以可行,是因為 Object
是 RegExp
的超類型
interface StringAndNumberKeys {: string]: Object;
[key: number]: RegExp;
[key
}
// %inferred-type: (x: StringAndNumberKeys) =>
// { str: Object; num: RegExp; }
function f(x: StringAndNumberKeys) {
: x['abc'], num: x[123] };
return { str }
如果介面中同時有索引簽章和屬性及/或方法簽章,則索引屬性值的類型也必須是屬性值和/或方法類型的超類型。
interface I1 {: string]: boolean;
[key
// @ts-expect-error: Property 'myProp' of type 'number' is not assignable
// to string index type 'boolean'. (2411)
: number;
myProp
// @ts-expect-error: Property 'myMethod' of type '() => string' is not
// assignable to string index type 'boolean'. (2411)
myMethod(): string;
}
相反地,以下兩個介面不會產生錯誤
interface I2 {: string]: number;
[key: number;
myProp
}
interface I3 {: string]: () => string;
[keymyMethod(): string;
}
所有介面都描述為物件實例,並繼承自 `Object.prototype` 的屬性。
在以下範例中,類型為 `{}` 的參數 `x` 與回傳類型 `Object` 相容
function f1(x: {}): Object {
;
return x }
類似地,`{}` 有方法 `toString()`
function f2(x: {}): { toString(): string } {
;
return x }
舉例來說,考慮以下介面
interface Point {: number;
x: number;
y }
有兩種方式(還有其他方式)可以詮釋此介面
TypeScript 使用這兩種詮釋。為了探討其運作方式,我們將使用以下函式
function computeDistance(point: Point) { /*...*/ }
預設情況下,允許額外屬性 `z`
= { x: 1, y: 2, z: 3 };
const obj computeDistance(obj); // OK
但是,如果我們直接使用物件文字,則禁止額外屬性
// @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
為什麼物件文字有更嚴格的規則?它們提供對屬性金鑰中錯字的保護。我們將使用以下介面來展示這表示什麼意思。
interface Person {: string;
first?: string;
middle: string;
last
}function computeFullName(person: Person) { /*...*/ }
屬性 `middle` 是可選的,可以省略(可選屬性會在 本章後續 介紹)。對 TypeScript 而言,輸入錯誤看起來像是省略它並提供一個額外屬性。但是,它仍然會偵測到錯字,因為在此情況下不允許額外屬性
// @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'});
這個想法是,如果一個物件來自其他地方,我們可以假設它已經過審查,不會有任何錯字。然後我們可以不用那麼小心。
如果錯字不是問題,我們的目標應該是最大化彈性。考慮以下函式
interface HasYear {: number;
year
}
function getAge(obj: HasYear) {
= new Date().getFullYear();
const yearNow - obj.year;
return yearNow }
如果不允許傳遞給 getAge()
的大多數值的額外屬性,這個函式的實用性將非常有限。
如果一個介面是空的(或使用物件型別文字 {}
),則總是允許額外屬性
interface Empty { }
interface OneProp {: number;
myProp
}
// @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)
: OneProp = { myProp: 1, anotherProp: 2 };
const a: Empty = {myProp: 1, anotherProp: 2}; // OK const b
如果我們要強制執行一個物件沒有屬性,我們可以使用以下技巧(致謝:Geoff Goodman)
interface WithoutProperties {: string]: never;
[key
}
// @ts-expect-error: Type 'number' is not assignable to type 'never'. (2322)
: WithoutProperties = { prop: 1 };
const a: WithoutProperties = {}; // OK const b
如果我們要在物件文字中允許額外屬性,該怎麼辦?舉例來說,考慮介面 Point
和函式 computeDistance1()
interface Point {: number;
x: number;
y
}
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 });
一種選擇是將物件文字指定給中間變數
= { x: 1, y: 2, z: 3 };
const obj computeDistance1(obj);
第二種選擇是使用型別斷言
computeDistance1({ x: 1, y: 2, z: 3 } as Point); // OK
第三種選擇是改寫 computeDistance1()
,使其使用型別參數
function computeDistance2<P extends Point>(point: P) { /*...*/ }
computeDistance2({ x: 1, y: 2, z: 3 }); // OK
第四種選擇是擴充介面 Point
,使其允許額外屬性
interface PointEtc extends Point {: string]: any;
[key
}function computeDistance3(point: PointEtc) { /*...*/ }
computeDistance3({ x: 1, y: 2, z: 3 }); // OK
我們將繼續使用兩個範例,說明 TypeScript 不允許額外屬性是一個問題。
Incrementor
在這個範例中,我們想要實作一個 Incrementor
,但 TypeScript 不允許額外屬性 .counter
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)
: start,
counterinc() {
// @ts-expect-error: Property 'counter' does not exist on type
// 'Incrementor'. (2339)
.counter++;
this,
};
} }
唉,即使使用型別斷言,仍然有一個型別錯誤
function createIncrementor2(start = 0): Incrementor {
return {: start,
counterinc() {
// @ts-expect-error: Property 'counter' does not exist on type
// 'Incrementor'. (2339)
.counter++;
this,
}as Incrementor;
} }
我們可以為介面 Incrementor
新增一個索引簽章。或者——特別是在不可能的情況下——我們可以引入一個中間變數
function createIncrementor3(start = 0): Incrementor {
= {
const incrementor : start,
counterinc() {
.counter++;
this,
};
};
return incrementor }
.dateStr
以下比較函式可用於對具有屬性 .dateStr
的物件進行排序
function compareDateStrings(
: {dateStr: string}, b: {dateStr: string}) {
aif (a.dateStr < b.dateStr) {
+1;
return if (a.dateStr > b.dateStr) {
} else -1;
return
} else {0;
return
} }
例如,在單元測試中,我們可能希望使用物件文字直接呼叫這個函式。TypeScript 不允許我們這樣做,我們需要使用其中一種解決方法。
這些是 TypeScript 為透過各種方式建立的物件推論的型別
// %inferred-type: Object
= new Object();
const obj1
// %inferred-type: any
= Object.create(null);
const obj2
// %inferred-type: {}
= {};
const obj3
// %inferred-type: { prop: number; }
= {prop: 123};
const obj4
// %inferred-type: object
= Reflect.getPrototypeOf({}); const obj5
原則上,Object.create()
的傳回型別可能是 object
。然而,any
允許我們新增和變更結果的屬性。
如果我們在屬性名稱後加上問號 (?
),該屬性就是可選的。相同的語法用於將函式、方法和建構函式的參數標記為可選。在以下範例中,屬性 .middle
是可選的
interface Name {: string;
first?: string;
middle: string;
last }
因此,省略該屬性 (A 行) 是可以的
: Name = {first: 'Doe', last: 'Doe'}; // (A)
const john: Name = {first: 'Jane', middle: 'Cecily', last: 'Doe'}; const jane
undefined|string
.prop1
和 .prop2
有什麼不同?
interface Interf {?: string;
prop1: undefined | string;
prop2 }
可選屬性可以執行 undefined|string
能執行的所有功能。我們甚至可以使用值 undefined
來表示前者
: Interf = { prop1: undefined, prop2: undefined }; const obj1
不過,只有 .prop1
可以省略
: Interf = { prop2: undefined };
const obj2
// @ts-expect-error: Property 'prop2' is missing in type '{}' but required
// in type 'Interf'. (2741)
: Interf = { }; const obj3
如果我們想要明確表示省略,可以使用 undefined|string
和 null|string
等類型。當人們看到這樣明確省略的屬性時,他們知道該屬性存在,但已關閉。
在以下範例中,屬性 .prop
是唯讀的
interface MyInterface {readonly prop: number;
}
因此,我們可以讀取它,但無法變更它
: MyInterface = {
const obj: 1,
prop;
}
console.log(obj.prop); // OK
// @ts-expect-error: Cannot assign to 'prop' because it is a read-only
// property. (2540)
.prop = 2; obj
TypeScript 沒有區分自有屬性和繼承屬性。它們都被視為屬性。
interface MyInterface {toString(): string; // inherited property
: number; // own property
prop
}: MyInterface = { // OK
const obj: 123,
prop; }
obj
從 Object.prototype
繼承 .toString()
。
這種方法的缺點是,JavaScript 中的一些現象無法透過 TypeScript 的類型系統來描述。優點是類型系統較為簡單。