TypeScript 对象类型
对象类型
在TypeScript中,我们通过对象类型(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;
}
属性修饰符
可选属性
我们可以在属性名后面加一个?
标记这个属性是可选的:
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 });
我们也可以尝试读取这些属性,但如果是在strictNullChecks
模式下,TypeScript会提示我们,属性值可能是undefined
。
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos;
// (property) PaintOptions.xPos?: number | undefined
let yPos = opts.yPos;
// (property) PaintOptions.yPos?: number | undefined
}
readonly
属性
在TypeScript中,属性可以被标记为readonly
,这不会改变任何运行时的行为,但在类型检查的时候,一个被标记为readonly
的属性是不能被写入的。
个人理解为,标记为readonly
的属性,只是储存在栈空间中的值或者引用地址不能变,也就是不能被重新赋值。
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
console.log(`Happy birthday ${home.resident.name}!`);
// 改变引用类型内部的属性是可以的
home.resident.age++;
}
function evict(home: Home) {
// 重新赋值会报错
home.resident = {
name: "Victor the Evictor",
age: 42,
};
}
还有一种情况就是,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,
};
let readonlyPerson: ReadonlyPerson = writablePerson;
// 这里由于是将writablePerson的地址赋值给了readonlyPerson
console.log(readonlyPerson.age); // prints '42'
// 所以这里修改writablePerson 必然会影响到readonlyPerson
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
索引签名
有时候我们并不能提前知道一个类型里所有属性的名字,但是我们知道这些值的特征,这种情况下就可以使用一个索引签名来描述可能的值的类型:
// 这个索引签名表示当一个StringArray类型的值使用number类型的值进行索引时,返回的一定是string类型的数据。
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1]; // const secondItem: string
注意:索引签名参数类型必须是 string
、number
、symbol
或模板文本类型。
数字索引的返回类型一定要是字符索引返回类型的子类型。这是因为当使用一个数字进行索引的时候,JavaScript 实际上把它转成了一个字符串。这就意味着使用数字 100 进行索引跟使用字符串 100 索引,是一样的。
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// Error: 用数字字符串索引可能会得到一个完全独立的Animal!
interface NotOkay {
[x: number]: Animal;
// “number”索引类型“Animal”不能赋值给“string”索引类型“Dog”
[x: string]: Dog;
}
索引签名还会强制要求所有属性都要匹配索引签名的返回类型:
interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string;
// 类型“string”的属性“name”不能赋给“string”索引类型“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;
}
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
// 类型“ReadonlyStringArray”中的索引签名仅允许读取。
属性继承
在TypeScript中可以使用extends关键字继承接口(interface):
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
// 可以继承多个类型 使用,分隔
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
交叉类型
在TypeScript中可以使用&
符号,用于合并已存在的对象类型:
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
// okay
draw({ color: "blue", radius: 42 });
接口继承与交叉类型
这两种方式在合并类型上看起来很相似,但实际上还是有很大不同。最原则性的不同就是在于冲突怎么处理,这也是你决定选择哪种方式的主要原因。
interface Colorful {
color: string;
}
interface ColorfulSub extends Colorful {
color: number
}
// 报错
// 接口“ColorfulSub”错误扩展接口“Colorful”。
// 属性“color”的类型不兼容。
// 不能将类型“number”分配给类型“string”。
使用继承的方式,如果重写类型会导致编译错误,但是交叉类型不会:
interface Colorful {
color: string;
}
type ColorfulSub = Colorful & {
color: number
}
// 虽然不会报错,但color最终的类型取得是string和number的交集。也就是never
泛型对象类型
在实际开发中,我们对象类型内的属性类型不一定会预先全部都知道,或者说我们想自由度更高,亦或者是所有类型都适用于这个对象内的属性,那我们就可以使用泛型。
interface Box<T> {
content: T,
}
let box: Box<string>;
把Box
想象成一个实际类型的模板,Type
就是一个占位符,可以被替代为具体的类型。当TypeScript看到Box<string>
,它就会替换为Box<Type>
的Type
为string
,最后的结果就会变成{ content: string }
这样做的好处是我们可以复用这个接口,需要不同类型的时候,只需要传入不同的类型去替换Type
就好。
类型别名也可以使用泛型:
type Box<Type> = {
contents: Type;
};
let box: Box<number>;
类型别名不同于接口,可以描述的不只是对象类型,所以可以使用类型别名写一些其他种类的泛型帮助类型。
Array
类型
在描述数组类型时,经常使用的是简写方法number[]
或者string[]
,其实他们只是Array<number>
和Array<string>
的简写形式而已。类似于上面举过的Box
类型,Array
本身就是一个泛型对象类型:
interface Array<T> {
length: number;
pop(): T | undefined;
push(...items: T[]): number;
}
现代 JavaScript 也提供其他是泛型的数据结构,比如 Map<K, V>
, Set<T>
和 Promise<T>
。因为 Map
、Set
、Promise
的行为表现,它们可以跟任何类型搭配使用。
ReadonlyArray
类型
ReadonlyArray
用来描述一个数组不能被改变:
function doStuff(values: ReadonlyArray<string>) {
// 我们只可以读取values
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// 但不能更改数组values.
values.push("hello!");
// 类型“readonly string[]”上不存在属性“push”。
}
ReadonlyArray
主要是用来做意图声明。当我们看到一个函数返回 ReadonlyArray
,就是在告诉我们不能去更改其中的内容,当我们看到一个函数支持传入 ReadonlyArray
,这是在告诉我们我们可以放心的传入数组到函数中,而不用担心会改变数组的内容。
同样的,ReadonlyArray<Type>
也有简写方法readonly Type[]
:
function fn(arr: readonly string[]) {
console.log(arr[0]);
arr.push('hello');// 报错:类型“readonly string[]”上不存在属性“push”
}
注意:Array
和ReadonlyArray
并不能双向赋值:
let x: readonly string[] = [];
let y: string[] = [];
x = y; // ok
y = x; // 类型 "readonly string[]" 为 "readonly",不能分配给可变类型 "string[]"。
我个人理解的是,因为readonly
描述的是数组内部的项是不能更改的,所以当y
赋值给x
时,此时x
是只读数组,但其实x
与y
此时栈中储存的地址指向的是同一块内存空间,此时如果改变数组y
,x
也会相应被更改,而上面y = x
会报错我想是基于纯粹的类型冲突。
let x: readonly string[] = [];
let y: string[] = [];
console.log('x:', x); // x: []
console.log('y:', y); // y: []
x = y; // ok
y.push('1');
console.log('x:', x); // x: [ '1' ]
console.log('y:', y); // y: [ '1' ]