本篇整理自 TypeScript Handbook 中 「Object Types」 章节。
对象类型(Object types)
对象类型的定义是可以匿名的
function greet(person: { name: string; age: number }) {
return "Hello " + person.name;
}
也可以使用接口进行定义
interface Person {
name: string;
age: number;
}
function greet(person: Person) {
return "Hello " + person.name;
}
或者通过类型别名
type Person = {
name: string;
age: number;
};
function greet(person: Person) {
return "Hello " + person.name;
}
属性修饰符(Property Modifiers)
可选属性(Optional Properties)
我们可以在属性名后面加一个 ? 标记表示这个属性是可选的
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
在 JavaScript 中,如果一个属性值没有被设置,我们获取会得到 undefined
这种判断在 JavaScript 中很常见,以至于提供了专门的语法糖
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos); // (parameter) xPos: number
console.log("y coordinate at", yPos); // (parameter) yPos: number
// ...
}
但要注意的是注意现在并没有在解构语法里放置类型注解的方式。
这是因为在 JavaScript 中,以下的写法会被认为是为解构变量起别名
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape);
// Cannot find name 'shape'. Did you mean 'Shape'?
render(xPos);
// Cannot find name 'xPos'.
}
readonly 属性(readonly Properties)
在 TypeScript 中,属性可以被标记为 readonly,这不会改变任何运行时的行为,但在类型检查的时候,一个标记为 readonly的属性是不能被写入的
interface SomeType {
readonly prop: string;
}
const foo: SomeType = {
prop: 'prop'
}
foo.prop = 123 // error
不过使用 readonly 并不意味着一个值就完全是不变的,亦或者说,内部的内容是不能变的。readonly 仅仅表明属性本身是不能被重新写入的
interface Home {
readonly resident: { name: string; age: number };
}
const home: Home = {
resident: {
name: 'Klaus',
age: 23
}
}
home.resident.age = 18 // succcss
home.resident = { // error
name: 'Alex',
age: 24
}
TypeScript 在检查两个类型是否兼容的时候,并不会考虑两个类型里的属性是否是 readonly,这就意味着,readonly 的值是可以通过别名修改的
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
索引签名(Index Signatures)
有的时候,你不能提前知道一个类型里的所有属性的名字,但是你知道这些值的特征。
这种情况,你就可以用一个索引签名 (index signature) 来描述可能的值的类型
interface StringArray {
[index: number]: string;
}
一个索引签名的属性类型必须是 string 或者是 number
但数字索引的返回类型一定要是字符索引返回类型的子类型。
这是因为当使用一个数字进行索引的时候,JavaScript 实际上把它转成了一个字符串。
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
// 'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
[x: string]: Dog;
}
尽管字符串索引用来描述字典模式(对象)非常的有效,但也会强制要求所有的属性要匹配索引签名的返回类型。
interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string; // error
// Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}
然而,如果一个索引签名是属性类型的联合,那各种类型的属性就可以接受了
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
你也可以设置索引签名为 readonly
interface ReadonlyStringArray {
readonly [index: number]: string;
}
属性继承(Extending Types)
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}
对接口使用 extends关键字允许我们有效的从其他声明过的类型中拷贝成员,并且随意添加新成员。
接口也可以继承多个类型
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
}
交叉类型(Intersection Types)
TypeScript 也提供了名为交叉类型(Intersection types)的方法,用于合并已经存在的对象类型。
交叉类型的定义需要用到 & 操作符
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
// 我们连结 Colorful 和 Circle 产生了一个新的类型,新类型拥有 Colorful 和 Circle 的所有成员
type ColorfulCircle = Colorful & Circle;
接口继承与交叉类型(Interfalces vs Intersections)
最原则性的不同就是在于冲突怎么处理,这也是你决定选择那种方式的主要原因
interface Colorful {
color: string;
}
interface ColorfulSub extends Colorful {
color: number
}
// Interface 'ColorfulSub' incorrectly extends interface 'Colorful'.
// Types of property 'color' are incompatible.
// Type 'number' is not assignable to type 'string'
使用继承的方式,如果重写类型会导致编译错误,但交叉类型不会
interface Colorful {
color: string;
}
// 此时color的类型是 never,取得是 string 和 number 的交集
type ColorfulSub = Colorful & {
color: number
}
泛型对象类型(Generic Object Types)
// 定义
interface Box<Type> {
contents: Type;
}
// 调用
let box: Box<string>;
Array 类型(The Array Type)
当我们这样写类型 number[] 或者 string[] 的时候,其实它们只是 Array<number> 和 Array<string> 的简写形式而已。
function doSomething(value: Array<string>) {
// ...
}
let myArray: string[] = ["hello", "world"];
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));
ReadonlyArray 类型(The ReadonlyArray Type)
ReadonlyArray 是一个特殊类型,它可以描述数组不能被改变。
ReadonlyArray 主要是用来做意图声明。当我们看到一个函数返回 ReadonlyArray,
就是在告诉我们不能去更改其中的内容, ReadonlyArray并不会影响运行时
我们可以直接把一个常规数组赋值给 ReadonlyArray
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
TypeScript 也针对 ReadonlyArray<Type> 提供了更简短的写法 readonly Type[]
function doStuff(values: readonly string[]) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");
// Property 'push' does not exist on type 'readonly string[]'.
}
最后有一点要注意,就是 Arrays 和 ReadonlyArray 并不能双向的赋值
let x: readonly string[] = [];
let y: string[] = [];
x = y; // ok
y = x; // error
// The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
元组
元组类型是另外一种 Array 类型,当你明确知道数组包含多少个元素,并且每个位置元素的类型都明确知道的时候,就适合使用元组类型
跟 ReadonlyArray 一样,它并不会在运行时产生影响
在元组类型中,你也可以写一个可选属性,但可选元素必须在最后面,而且也会影响类型的 length
type Either2dOr3d = [number, number, number?];
function setCoordinate(coord: Either2dOr3d) {
const [x, y, z] = coord;
console.log(`Provided coordinates had ${coord.length} dimensions`);
// (property) length: 2 | 3
}
Tuples 也可以使用剩余元素语法,但必须是 array/tuple 类型
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...[string, number], number];
有剩余元素的元组并不会设置 length,因为它只知道在不同位置上的已知元素信息
type StringNumberBooleans = [string, number, ...boolean[]];
const a: StringNumberBooleans = ["hello", 1];
console.log(a.length); // (property) length: number
type StringNumberPair = [string, number];
const d: StringNumberPair = ['1', 1];
console.log(d.length); // (property) length: 2
readonly 元组类型(readonly Tuple Types)
元组类型也是可以设置 readonly 的
function doSomething(pair: readonly [string, number]) {
// ...
}
在大部分的代码中,元组只是被创建,使用完后也不会被修改,所以尽可能的将元组设置为 readonly 是一个好习惯
如果我们给一个数组字面量 const 断言,也会被推断为 readonly 元组类型
let point = [3, 4] as const;
function distanceFromOrigin([x, y]: [number, number]) {
return Math.sqrt(x ** 2 + y ** 2);
}
distanceFromOrigin(point);
// 尽管 distanceFromOrigin 并没有更改传入的元素,但函数希望传入一个可变元组。因为 point 的类型被推断为 readonly [3, 4],它跟 [number number] 并不兼容,所以 TypeScript 给了一个报错