写在前面
对于对象等复杂结构的类型,TypeScript的理念是鸭子类型(duck typing),即值的“形状”:
Type-checking focuses on the shape that values have.
TypeScript里,通过接口来描述复杂结构的类型,例如:
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
// 等价于
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
这里接口的概念不同于其它语言,不必显式实现,只表示一种类型约束
一.对象
可选属性
紧跟着属性名的?
表示可选,类似于正则表达式中?
的含义,例如:
interface SquareConfig {
color?: string; // 颜色可选
width: number; // 宽度必填
}
声明可选的意义在于,不使用没关系,用的话约束其类型正确:
// 正确
let squareConfig: SquareConfig = {width: 10};
// 错误:Type '10' is not assignable to type 'string'.
squareConfig.color = 10;
特殊的:
// 错误:Type '{ colour: string; width: number; }' is not assignable to type 'SquareConfig'.
let a: SquareConfig = { colour: "red", width: 100 };
拼写有误的colour
之所以能被发现,是因为会检查对象字面量身上的多余属性:
If an object literal has any properties that the “target type” doesn’t have, you’ll get an error.
但这种检查只针对字面量,因此:
let squareOptions = { colour: "red", width: 100 };
// 正确,不检查变量squareOptions身上的多余属性
let a: SquareConfig = squareOptions;
索引签名
有些场景下无法确定属性名称,例如:
let cache: NetCache = {};
cache['http://example.com'] = 'response';
cache['http://example.com/second'] = 'response';
允许NetCache
类型的对象具有任意多个名为字符串的属性,此时可以通过索引签名(index signature)来描述这种类型约束:
interface NetCache {
[propName: string]: string;
}
只读属性
interface Point {
readonly x: number;
readonly y: number;
}
紧跟在属性名前的readonly
表示只读,与const
约束一样,修改只读属性会抛出编译错误:
let p1: Point = { x: 10, y: 20 };
// 错误:Cannot assign to 'x' because it is a read-only property.
p1.x = 5;
P.S.const
与readonly
的区别在于前者用来约束变量,后者用来约束属性(变量声明之外的场景)
特殊的,只读数组有一种特别的类型表示ReadonlyArray<T>
:
let ro: ReadonlyArray<number> = [1, 2, 3, 4];
// 都会引发编译报错
ro[0] = 5;
ro.push(5);
ro.length = 1;
限制了其它所有会让数组内容发生变化的方式,还去掉了原型上的修改方法(pop
、push
、reverse
、shift
等),因此不允许把只读数组赋值给普通数组:
// Type 'ReadonlyArray<number>' is missing the following properties from type 'number[]': pop, push, reverse, shift, and 6 more.
let arr: number[] = ro;
P.S.非要赋值的话,可以通过类型断言来做(let a: number[] = ro as number[]
)
另外,readonly
也可以结合索引签名使用,例如:
interface NetCache {
readonly [propName: string]: string;
}
用来约束属性值初始化之后无法修改:
let cache: NetCache = { 'http://example.com': 'response' };
// Index signature in type 'NetCache' only permits reading.
cache['url'] = 'response';
二.函数
接口也能用来表示函数类型,通过调用签名(call signature)来描述:
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc = (source: string, subString: string) => {
let result = source.search(subString);
return result > -1;
};
函数类型会对2个东西进行检查:
参数类型
返回值类型
注意,参数名不必完全匹配(不要求参数名一定是source
和subString
,按参数位置依次检查)
三.数组
数组的类型也可以用接口表示,例如:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
没错,就是索引签名,不仅能表示一类属性,还能描述数组。之所以叫索引签名,是因为它能够描述可索引值的类型,例如StringArray
表示能够通过数值索引访问字符串值
注意,只有两种合法的索引签名,分别是string
和number
,并且二者不能同时出现:
interface NotOkay {
// Numeric index type 'boolean' is not assignable to string index type 'string'.
[x: number]: boolean;
[x: string]: string;
}
这是因为JavaScript中数值索引会被转换成字符串索引:
// JavaScript
const a = [1, 2, 3];
a[1] === a['1'] // true
四.类
与其它语言一样,类与接口之间有实现(implements
)关系:
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
接口就像一种协议(或者说是契约),用来描述类的公开成员:
Explicitly enforcing that a class meets a particular contract.
P.S.构造函数的类型也能用接口描述,具体见Difference between the static and instance sides of classes
五.接口继承
接口可以通过继承的方式来扩展,例如:
interface Shape {
color: string;
}
// 通过继承获得color属性
interface Square extends Shape {
sideLength: number;
}
同样,也支持多继承:
interface PenStroke {
penWidth: number;
}
// 多继承
interface Square extends Shape, PenStroke {
sideLength: number;
}
通过继承建立的这种层级关系有助于组织有关联的接口,实现拆分、复用
P.S.特殊的,接口可以继承自类,相当于把该类的所有类型声明(包括私有属性)抽出来作为接口,用于约束子类,具体见Interfaces Extending Classes
六.混合类型
JavaScript里,函数也能像对象一样具有属性:
require('./utils');
delete require.cache[require.resolve('./utils')];
从类型上看,同时具有函数和对象的特征,称之为混合类型:
interface NodeRequireFunction {
/* tslint:disable-next-line:callable-types */
(id: string): any;
}
interface NodeRequire extends NodeRequireFunction {
resolve: RequireResolve;
cache: any;
extensions: NodeExtensions;
main: NodeModule | undefined;
}