Object 与 objectObject 的实例Object(大写字母“O”):类 Object 的实例object(小写字母“o”):非原始值Object 与 object:原始值Object 与 object:不兼容的属性类型Object 的实例在本章中,我们将探讨如何在 TypeScript 中对对象和属性进行静态类型化。
在 JavaScript 中,对象可以扮演两种角色(始终至少扮演其中一种角色,有时是混合角色)
记录 具有在开发时已知的固定数量的属性。每个属性可以具有不同的类型。
字典 具有任意数量的属性,其名称在开发时未知。所有属性键(字符串和/或符号)都具有相同的类型,属性值也一样。
首先,我们将探讨作为记录的对象。我们将在 本章后面 简要介绍作为字典的对象。
对象有两种不同的通用类型
大写字母“O”的 Object 是类 Object 的所有实例的类型
let obj1: Object;小写字母“o”的 object 是所有非原始值的类型
let obj2: object;对象也可以通过其属性进行类型化
// Object type literal
let obj3: {prop: boolean};
// Interface
interface ObjectType {
prop: boolean;
}
let obj4: ObjectType;在接下来的部分中,我们将更详细地研究所有这些对对象进行类型化的方式。
Object 与 objectObject 的实例在纯 JavaScript 中,有一个重要的区别。
一方面,大多数对象都是 Object 的实例。
> const obj1 = {};
> obj1 instanceof Object
true这意味着
Object.prototype 在它们的原型链中
> Object.prototype.isPrototypeOf(obj1)
true它们继承了它的属性。
> obj1.toString === Object.prototype.toString
true另一方面,我们也可以创建原型链中没有 Object.prototype 的对象。例如,以下对象根本没有任何原型
> const obj2 = Object.create(null);
> Object.getPrototypeOf(obj2)
nullobj2 是一个不是类 Object 实例的对象
> typeof obj2
'object'
> obj2 instanceof Object
falseObject(大写字母“O”):类 Object 的实例回想一下,每个类 C 都会创建两个实体
C。C。类似地,TypeScript 有两个内置接口
接口 Object 指定 Object 实例的属性,包括从 Object.prototype 继承的属性。
接口 ObjectConstructor 指定类 Object 的属性。
这些是接口
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)观察结果
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.hasOwnProperty
true相反,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)
const obj1: Object = { toString() { return 123 } };对于类型 object,TypeScript 不会发出警告(因为 object 没有指定任何属性,并且不会有任何冲突)
const obj2: object = { toString() { return 123 } };TypeScript 有两种定义对象类型的方式,它们非常相似
// Object type literal
type ObjType1 = {
a: boolean,
b: number;
c: string,
};
// Interface
interface ObjType2 {
a: boolean,
b: number;
c: string,
}我们可以使用分号或逗号作为分隔符。允许使用尾随分隔符,并且是可选的。
在本节中,我们将了解对象类型字面量和接口之间最重要的区别。
对象类型字面量可以内联,而接口不能
// Inlined object type literal:
function f1(x: {prop: number}) {}
// Referenced interface:
function f2(x: ObjectInterface) {}
interface ObjectInterface {
prop: number;
}具有重复名称的类型别名是非法的
// @ts-expect-error: Duplicate identifier 'PersonAlias'. (2300)
type PersonAlias = {first: string};
// @ts-expect-error: Duplicate identifier 'PersonAlias'. (2300)
type PersonAlias = {last: string};相反,具有重复名称的接口会被合并
interface PersonInterface {
first: string;
}
interface PersonInterface {
last: string;
}
const jane: PersonInterface = {
first: 'Jane',
last: 'Doe',
};对于映射类型(A 行),我们需要使用对象类型字面量
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];
// }; 有关映射类型的更多信息
映射类型超出了本书当前的范围。有关更多信息,请参阅 TypeScript 手册。
this 类型多态 this 类型只能在接口中使用
interface AddsStrings {
add(str: string): this;
};
class StringBuilder implements AddsStrings {
result = '';
add(str: string) {
this.result += str;
return this;
}
} 从现在开始,“接口”表示“接口或对象类型字面量”(除非另有说明)。
接口是结构化的——它们不必为了匹配而实现
interface Point {
x: number;
y: number;
}
const point: Point = {x: 1, y: 2}; // OK有关此主题的更多信息,请参阅 [未包含的内容]。
接口和对象类型字面量主体内的构造称为它们的成员。以下是它们最常见的成员
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 {}让我们更详细地了解这些成员
属性签名定义属性
myProperty: boolean;方法签名定义方法
myMethod(str: string): number;注意:参数的名称(在本例中为 str)有助于记录工作原理,但没有其他用途。
索引签名是描述用作字典的数组或对象所必需的。
[key: string]: any;注意:名称 key 仅用于文档目的。
调用签名使接口能够描述函数
(num: number): string;构造签名使接口能够描述类和构造函数
new(str: string): ExampleInstance; 属性签名应该是自解释的。 调用签名 和 构造签名 将在本书后面介绍。接下来,我们将仔细研究方法签名和索引签名。
就 TypeScript 的类型系统而言,方法定义和值为函数的属性是等效的
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;我的建议是使用最能表达属性应如何设置的语法。
到目前为止,我们只将接口用于具有固定键的作为记录的对象。我们如何表达一个对象将被用作字典的事实?例如:在以下代码片段中,TranslationDict 应该是什么?
function translate(dict: TranslationDict, english: string): string {
return dict[english];
}我们使用索引签名(A 行)来表示 TranslationDict 用于将字符串键映射到字符串值的对象
interface TranslationDict {
[key:string]: string; // (A)
}
const dict = {
'yes': 'sí',
'no': 'no',
'maybe': 'tal vez',
};
assert.equal(
translate(dict, 'maybe'),
'tal vez');索引签名键必须是 string 或 number
any。string|number)。但是,每个接口可以使用多个索引签名。就像在纯 JavaScript 中一样,TypeScript 的数字属性键是字符串属性键的子集(请参阅“面向不耐烦程序员的 JavaScript”)。因此,如果我们同时具有字符串索引签名和数字索引签名,则前者的属性类型必须是后者的超类型。以下示例有效,因为 Object 是 RegExp 的超类型
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] };
}如果在一个接口中同时存在索引签名和属性和/或方法签名,则索引属性值的类型也必须是属性值和/或方法类型的超类型。
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;
}相反,以下两个接口不会产生错误
interface I2 {
[key: string]: number;
myProp: number;
}
interface I3 {
[key: string]: () => string;
myMethod(): string;
}Object 的实例所有接口都描述了 Object 的实例并继承了 Object.prototype 属性的对象。
在以下示例中,类型为 {} 的参数 x 与返回类型 Object 兼容
function f1(x: {}): Object {
return x;
}类似地,{} 具有方法 .toString()
function f2(x: {}): { toString(): string } {
return x;
}例如,请考虑以下接口
interface Point {
x: number;
y: number;
}可以通过两种方式(以及其他方式)来解释此接口
.x 和 .y 的所有对象。换句话说:这些对象不得具有多余属性(超过所需属性)。.x 和 .y 的所有对象。换句话说:允许存在多余属性。TypeScript 同时使用这两种解释。为了探讨它是如何工作的,我们将使用以下函数
function computeDistance(point: Point) { /*...*/ }默认情况下,允许存在多余属性 .z
const obj = { x: 1, y: 2, z: 3 };
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 {
first: string;
middle?: string;
last: string;
}
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 {
year: number;
}
function getAge(obj: HasYear) {
const yearNow = new Date().getFullYear();
return yearNow - obj.year;
}如果不允许传递给 getAge() 的大多数值存在多余属性,那么此函数的用处将非常有限。
如果一个接口是空的(或者使用了对象类型字面量 {}),则始终允许额外的属性。
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如果我们想强制一个对象没有属性,可以使用以下技巧(感谢: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如果我们想在对象字面量中允许额外的属性怎么办?例如,考虑接口 Point 和函数 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 });一种选择是将对象字面量赋值给一个中间变量。
const obj = { x: 1, y: 2, z: 3 };
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 {
[key: string]: any;
}
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)
counter: start,
inc() {
// @ts-expect-error: Property 'counter' does not exist on type
// 'Incrementor'. (2339)
this.counter++;
},
};
}唉,即使使用类型断言,仍然有一个类型错误。
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;
}我们可以向接口 Incrementor 添加索引签名。或者 - 特别是在不可能的情况下 - 我们可以引入一个中间变量。
function createIncrementor3(start = 0): Incrementor {
const incrementor = {
counter: start,
inc() {
this.counter++;
},
};
return incrementor;
}.dateStr以下比较函数可用于对具有属性 .dateStr 的对象进行排序。
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;
}
}例如,在单元测试中,我们可能希望直接使用对象字面量调用此函数。TypeScript 不允许我们这样做,我们需要使用一种解决方法。
这些是 TypeScript 为通过各种方式创建的对象推断出的类型。
// %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({});原则上,Object.create() 的返回类型可以是 object。但是,any 允许我们添加和更改结果的属性。
如果我们在属性名称后面加上一个问号 (?),则该属性是可选的。相同的语法用于将函数、方法和构造函数的参数标记为可选。在以下示例中,属性 .middle 是可选的。
interface Name {
first: string;
middle?: string;
last: string;
}因此,可以省略该属性(A 行)。
const john: Name = {first: 'Doe', last: 'Doe'}; // (A)
const jane: Name = {first: 'Jane', middle: 'Cecily', last: 'Doe'};undefined|string.prop1 和 .prop2 之间有什么区别?
interface Interf {
prop1?: string;
prop2: undefined | string;
}可选属性可以做 undefined|string 可以做的所有事情。我们甚至可以对前者使用值 undefined。
const obj1: Interf = { prop1: undefined, prop2: undefined };但是,只有 .prop1 可以省略。
const obj2: Interf = { prop2: undefined };
// @ts-expect-error: Property 'prop2' is missing in type '{}' but required
// in type 'Interf'. (2741)
const obj3: Interf = { };如果我们想明确表示省略,则 undefined|string 和 null|string 等类型很有用。当人们看到这样一个被明确省略的属性时,他们知道它存在,但被关闭了。
在以下示例中,属性 .prop 是只读的。
interface MyInterface {
readonly prop: number;
}因此,我们可以读取它,但不能更改它。
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 不区分自身属性和继承属性。它们都被简单地视为属性。
interface MyInterface {
toString(): string; // inherited property
prop: number; // own property
}
const obj: MyInterface = { // OK
prop: 123,
};obj 从 Object.prototype 继承 .toString()。
这种方法的缺点是 JavaScript 中的某些现象无法通过 TypeScript 的类型系统来描述。优点是类型系统更简单。