Object
与 object
Object
的实例Object
(大写字母“O”):类 Object
的实例object
(小写字母“o”):非原始值Object
与 object
:原始值Object
与 object
:不兼容的属性类型Object
的实例在本章中,我们将探讨如何在 TypeScript 中对对象和属性进行静态类型化。
在 JavaScript 中,对象可以扮演两种角色(始终至少扮演其中一种角色,有时是混合角色)
记录 具有在开发时已知的固定数量的属性。每个属性可以具有不同的类型。
字典 具有任意数量的属性,其名称在开发时未知。所有属性键(字符串和/或符号)都具有相同的类型,属性值也一样。
首先,我们将探讨作为记录的对象。我们将在 本章后面 简要介绍作为字典的对象。
对象有两种不同的通用类型
大写字母“O”的 Object
是类 Object
的所有实例的类型
: Object; let obj1
小写字母“o”的 object
是所有非原始值的类型
: 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
、布尔值、数字、bigint、字符串)。使用此类型,我们无法访问值的任何属性。
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
的实例所有接口都描述了 Object
的实例并继承了 Object.prototype
属性的对象。
在以下示例中,类型为 {}
的参数 x
与返回类型 Object
兼容
function f1(x: {}): Object {
;
return x }
类似地,{}
具有方法 .toString()
function f2(x: {}): { toString(): string } {
;
return x }
例如,请考虑以下接口
interface Point {: number;
x: number;
y }
可以通过两种方式(以及其他方式)来解释此接口
.x
和 .y
的所有对象。换句话说:这些对象不得具有多余属性(超过所需属性)。.x
和 .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 的类型系统来描述。优点是类型系统更简单。