TypeScript中的对象
基本使用
匿名、interface、type
// 匿名
function greet(person: { name: string; age: number }) {
return "Hello " + person.name;
}
// interface
interface Person {
name: string;
age: number;
}
function greet(person: Person) {
return "Hello " + person.name;
}
// type
type Person = {
name: string;
age: number;
};
function greet(person: Person) {
return "Hello " + person.name;
}
// 匿名
function greet(person: { name: string; age: number }) {
return "Hello " + person.name;
}
// interface
interface Person {
name: string;
age: number;
}
function greet(person: Person) {
return "Hello " + person.name;
}
// type
type Person = {
name: string;
age: number;
};
function greet(person: Person) {
return "Hello " + person.name;
}
属性修饰符
对象类型中的每个属性都可以指定以下几点:类型
、属性是否可选
以及是否可以写入
属性。
可选属性
通过在名称末尾添加问号 ?
将这些属性标记为可选。
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
function paintShape(opts: PaintOptions) {
// ...
}
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
function paintShape(opts: PaintOptions) {
// ...
}
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });
在此示例中,xPos 和 yPos 都被视为可选。我们可以选择提供其中任何一个,因此上面对 paintShape 的每个调用都是有效的。所有可选性实际上都表明,如果设置了属性,它最好具有特定类型。
但是当我们在 开启 strictNullCheck
模式 下读取时,TypeScript 会告诉我们它们可能是 undefined。
我们可以为其设置默认参数。或者使用 ?
!
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos);
console.log("y coordinate at", yPos);
}
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos);
console.log("y coordinate at", yPos);
}
在这里,我们对 paintShape 的参数使用了解构模式,并为 xPos 和 yPos 提供了默认值。现在,xPos 和 yPos 都明确存在于 paintShape 的主体中,但对于 paintShape 的任何调用方来说都是可选的。
TIP
请注意,目前没有办法在解构模式中放置类型注释。这是因为以下语法在 JavaScript 中已经表示不同的东西。
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape); // Cannot find name 'shape'. Did you mean 'Shape'?
render(xPos); // Cannot find name 'xPos'.
}
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape); // Cannot find name 'shape'. Did you mean 'Shape'?
render(xPos); // Cannot find name 'xPos'.
}
在对象解构模式中,shape: Shape 表示“获取属性 shape 并将其在本地重新定义为名为 Shape 的变量”。同样,xPos: number 会创建一个名为 number 的变量,其值基于参数的 xPos。
只读属性
还可以将 TypeScript 的属性标记为只读。虽然它不会在运行时更改任何行为,但在类型检查期间无法写入标记为 readonly 的属性。
interface SomeType {
readonly prop: string;
}
function doSomething(obj: SomeType) {
console.log(`prop has the value '${obj.prop}'.`); // ok
obj.prop = "hello"; // 报错:Cannot assign to 'prop' because it is a read-only property.
}
interface SomeType {
readonly prop: string;
}
function doSomething(obj: SomeType) {
console.log(`prop has the value '${obj.prop}'.`); // ok
obj.prop = "hello"; // 报错:Cannot assign to 'prop' because it is a read-only property.
}
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
home.resident.age++; // ok
home.resident = {}; // 报错
}
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
home.resident.age++; // ok
home.resident = {}; // 报错
}
管理对 readonly 含义的期望非常重要。在 TypeScript 的开发期间,向 TypeScript 发出应如何使用对象的意图信号非常有用。
- readonly 属性也可以通过别名进行更改。
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
let readonlyPerson: ReadonlyPerson;
readonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
let readonlyPerson: ReadonlyPerson;
readonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
- 使用映射修饰符,您可以删除 readonly 属性。
索引签名
有时,您事先并不知道类型属性的所有名称,但您确实知道值的形状。在这些情况下,您可以使用索引签名来描述可能值的类型。
例如:数组
interface MyArr {
[index: number]: string;
}
const arr: MyArr = ["Echo", "James", "John", "Steven"]; // 约束的是这个数组的数据
console.log(arr[0]); // Echo
console.log(arr[1]); // James
console.log(arr[2]); // John
console.log(arr[3]); // Steven
interface MyArr {
[index: number]: string;
}
const arr: MyArr = ["Echo", "James", "John", "Steven"]; // 约束的是这个数组的数据
console.log(arr[0]); // Echo
console.log(arr[1]); // James
console.log(arr[2]); // John
console.log(arr[3]); // Steven
在上面,我们有一个 MyArr 接口,它有一个索引签名。此索引签名表示,当 MyArr 使用数字编制索引时,它将返回一个字符串。
例如:对象
interface MyObj {
[index: string]: {message: string};
}
const obj: MyObj = { // 约束的是这个对象的数据
'a': {message: 'A'},
'b': {message: 'B'},
};
console.log(obj['a']); // { "message": "A" }
console.log(obj['b']); // { "message": "B" }
interface MyObj {
[index: string]: {message: string};
}
const obj: MyObj = { // 约束的是这个对象的数据
'a': {message: 'A'},
'b': {message: 'B'},
};
console.log(obj['a']); // { "message": "A" }
console.log(obj['b']); // { "message": "B" }
索引签名属性只允许使用某些类型:string
、number
、symbol
、模板字符串模式
和仅包含这些类型的联合类型
。
虽然字符串索引签名是描述 “字典” 模式的有效方法,但它们还强制所有属性都与其返回类型匹配。这是因为字符串索引声明 obj.property 也可用作 obj[“property”]。
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number 可以兼容上面的 number | string;
name: string; // ok, name is a string 可以兼容上面的 number | string;
sex: boolean // 报错: boolean 无法兼容上面的 number | string;
}
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number 可以兼容上面的 number | string;
name: string; // ok, name is a string 可以兼容上面的 number | string;
sex: boolean // 报错: boolean 无法兼容上面的 number | string;
}
上面示例中,sex 的类型与字符串索引的类型不匹配,并且类型检查器会给出错误。
例如:多个索引
可以支持多种类型的索引器。请注意,当同时使用 'number' 和 'string' 索引器时,从数字索引器返回的类型必须是从字符串索引器返回的类型的子类型。这是因为当使用数字进行索引时,JavaScript 实际上会在索引到对象之前将其转换为字符串。这意味着使用 100(一个数字)进行索引与使用 “100”(字符串)进行索引是一回事,因此两者需要保持一致。
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// ×
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
// 报错:'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
// 因为:从数字索引器返回的类型必须是从字符串索引器返回的类型的子类型
// √
interface isOkay {
[x: string]: Animal;
[x: number]: Dog;
}
// 索引 number 必须为string的子类型
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// ×
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
// 报错:'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
// 因为:从数字索引器返回的类型必须是从字符串索引器返回的类型的子类型
// √
interface isOkay {
[x: string]: Animal;
[x: number]: Dog;
}
// 索引 number 必须为string的子类型
将索引签名设为 readonly,以防止分配给其索引:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ['A','B'];
myArray[2] = "Mallory";
// 报错:Index signature in type 'ReadonlyStringArray' only permits reading.
// 因为:因为索引签名是只读的。
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ['A','B'];
myArray[2] = "Mallory";
// 报错:Index signature in type 'ReadonlyStringArray' only permits reading.
// 因为:因为索引签名是只读的。
超额属性检查
TypeScript 的超额属性检查有助于确保对象与预期形状匹配,从而避免杂散属性带来的潜在错误。
interface Person {
name: string;
age: number;
}
let john: Person = {
name: 'John Doe',
age: 30,
occupation: 'Developer' // TypeScript error: Object literal may only specify known properties...
};
interface Person {
name: string;
age: number;
}
let john: Person = {
name: 'John Doe',
age: 30,
occupation: 'Developer' // TypeScript error: Object literal may only specify known properties...
};
在上面的代码片段中,TypeScript 会抛出一个错误,因为 'Person' 接口没有 'occupation' 属性,但我们尝试在 'john' 对象声明中分配它。
为对象分配类型的位置和方式可能会对类型系统产生影响。其中一个关键示例是超额属性检查,在创建对象并在创建过程中将其分配给对象类型时,它会更彻底地验证对象。
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
return {
color: config.color || "red",
area: config.width ? config.width * config.width : 20,
};
}
let mySquare = createSquare({ colour: "red", width: 100 });
// 报错 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
return {
color: config.color || "red",
area: config.width ? config.width * config.width : 20,
};
}
let mySquare = createSquare({ colour: "red", width: 100 });
// 报错 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
请注意,createSquare 的给定参数拼写为 colour 而不是 color。在纯 JavaScript 中,这种事情会悄无声息地失败。
你可以认为这个程序的类型是正确的,因为 width 属性是兼容的,不存在 color 属性,额外的 colour 属性是没影响的。
但是,TypeScript 的立场是此代码中可能存在错误。对象字面量会得到特殊处理,并在将它们分配给其他变量或将它们作为参数传递时进行额外的属性检查。如果对象字面量具有 “target type” 所没有的任何属性,则会收到错误:
let mySquare = createSquare({ colour: "red", width: 100 });
// Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
let mySquare = createSquare({ colour: "red", width: 100 });
// Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
绕过检查的方式:
- 绕过这些检查实际上非常简单。最简单的方法是只使用类型断言:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
- 绕过这些检查的最后一种方法(可能有点令人惊讶)是将对象分配给另一个变量:由于分配 squareOptions 不会经过过多的属性检查,因此编译器不会给你一个错误:
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
扩展类型
接口上的 extends 关键字允许我们有效地从其他命名类型复制成员,并添加我们想要的任何新成员。
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}
这对于减少我们必须编写的类型声明样板的数量很有用,并且可以表明同一属性的几个不同声明可能相关。
接口还可以从多个类型扩展(类似于合并接口)。
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {} // ColorfulCircle 为空,只继承(类似于合并接口)
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {} // ColorfulCircle 为空,只继承(类似于合并接口)
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
交叉类型
交叉点类型是使用 &
运算符定义的。
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle; // 合并接口
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle; // 合并接口
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
在这里,我们将 Colorful 和 Circle 相交,以生成一个具有 Colorful和Circle 所有成员的新类型。
扩展 vs 交叉
- 两种组合类型的方法,它们相似(都可以用于合并接口),但实际上略有不同。
- 两者之间的主要区别在于冲突的处理方式。
interface Person1 {
name: string;
}
interface Person2 {
name: number;
}
interface Person3 extends Person1, Person2 {} // 报错
type Person4 = Person1 & Person2; // name 被处理 never 类型
interface Person1 {
name: string;
}
interface Person2 {
name: number;
}
interface Person3 extends Person1, Person2 {} // 报错
type Person4 = Person1 & Person2; // name 被处理 never 类型
泛型对象类型
TypeScript 泛型对象类型允许您为对象创建灵活且可重用的类型定义。这些泛型类型可以处理不同形状的对象,同时提供类型安全性,确保您的代码既健壮又适应性强。
type KeyValuePair<T> = {
key: string;
value: T;
};
const stringPair: KeyValuePair<string> = { key: 'name', value: 'John' };
const numberPair: KeyValuePair<number> = { key: 'age', value: 30 };
type KeyValuePair<T> = {
key: string;
value: T;
};
const stringPair: KeyValuePair<string> = { key: 'name', value: 'John' };
const numberPair: KeyValuePair<number> = { key: 'age', value: 30 };
值得注意的是,类型别名也可以是泛型的。我们本可以定义新的 Box<Type>
接口,即:
interface Box<Type> {
contents: Type;
}
type Box<Type> = {
contents: Type;
};
interface Box<Type> {
contents: Type;
}
type Box<Type> = {
contents: Type;
};
Array(泛型)
Array 本身是一个泛型类型。
interface Array<Type> {
length: number;
pop(): Type | undefined;
push(...items: Type[]): number;
}
interface Array<Type> {
length: number;
pop(): Type | undefined;
push(...items: Type[]): number;
}
ReadonlyArray
ReadonlyArray 是一种特殊类型,用于描述不应更改的数组。
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
// 数组 roArray 是不允许被修改的
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
// 数组 roArray 是不允许被修改的
正如 TypeScript 为具有 Type[] 的 Array<Type>
提供简写语法一样,它也为具有只读 Type[] 的 ReadonlyArray<Type>
提供简写语法。
const values: readonly string[] = [];
const values: readonly string[] = [];
最后要注意的一点是,与 readonly 属性修饰符不同,常规 Array和 ReadonlyArray之间的可分配性不是双向的。
let x: readonly string[] = [];
let y: string[] = [];
x = y;
y = x; // y 报错:The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'
let x: readonly string[] = [];
let y: string[] = [];
x = y;
y = x; // y 报错:The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'
元组类型
元组类型是另一种 Array 类型(固定的数组),它确切地知道它包含多少个元素,以及它在特定位置包含哪些类型。
type StringNumberPair = [string, number];
type StringNumberPair = [string, number];
元组也可以具有 rest 元素,这些元素必须是数组/元组类型。
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
元组类型具有 readonly 变体,可以通过在它们前面放置 readonly 修饰符来指定 - 就像数组速记语法一样。
function doSomething(pair: readonly [string, number]) {
// ...
}
function doSomething(pair: readonly [string, number]) {
// ...
}