接口(interface)
TypeScript的核心原则之一是对值的所具有的结构进行类型检查。所以只要是值就一定具有自己的数据结构,相比之前的一些基础类型,像对象和函数等看起来的数据结构就是复杂多变的。在TypeScript里,接口作用就是为这些复杂的类型命名,然后将这些值的数据结构定义成一种契约,使用这些值时就必须遵循相同的契约(接口)。
初探接口
function printLabel(labelObject: { label: string }): void {
console.log(labelObject.label);
}
let myObj = { size: 10, label: "SizeLabel" };
printLabel(myObj);
上面讲述了一个关于接口验证工作的例子。printLabel函数有一个参数labelObject,这个参数定义了一个空字面对象的类型定义,要求这个对象参数必须要有一个名为label且类型为string的属性。后续创建了一个myObj对象中一个number类型的size属性和一个string类型的label,所以满足了要求后,可以将myObj传入到printLabel中不会类型验证错误。如果将label的值也换成number类型就无法将参数传入了。
需要注意的是,我实际传入参数包含很多了属性,但是编译器只会检查那些必须的属性是否存在,且类型必须一致。如果编译器检查的是非必须的属性,那在实际中传入参数包含该属性的话,类型必须一致。
根据上面的示例使用接口重新改写一下:
interface LabelType {
label: string;
}
function printLabel(labelObject: LabelType) {
console.log(labelObject.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
LabelType接口就好比是一个契约,用来描述参数的要求。它代表了有一个label属性且类型为string的对象。
需要注意的是我们不用像其他语言一样,传给labelObject要实现LabelType这个接口,如myObj没有实现LabelType接口,因为myObj有多余的size属性。在TS中只会关注值的外形,只要传入的对象满足了参数的必要条件,那对象就是被允许的。
还有一点,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以了。
接口的可选属性
在上面的示例中提到非必须属性,说明接口里面的属性不全都是必须的。有些属性只是在某些条件下存在或者使用。可选属性在应用“option bags”模式时很常用,就是传入参数对象中只有部分属性赋值了。下面一个应用一个例子。
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: "white", area: 100 };
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({ color: "black" });
let mySquare2 = createSquare({ width: 100 });
let mySquare3 = createSquare({});
带有可选属性接口与普通接口定义差不多,可选属性写法是在名字属性定义后面加入?符号。可选属性有两个好处,第一个好处是对可能存在的属性进行预定义,第二个好处是可以捕获引用了不存在的属性时的错误。
接口的只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。可以在属性前面用readonly来指定只读属性,构建一个只读属性接口,在初始后构建完成,只读属性的值不能再次被改变。
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
TypeScript具有ReadonlyArray类型,它与Array相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。
let numberArray: number[] = [1, 2, 3, 4];
let readonlyArray: ReadonlyArray<number> = numberArray;
readonlyArray[0] = 12; // error!
readonlyArray.push(5); // error!
readonlyArray.length = 100; // error!
numberArray = readonlyArray; // error!
numberArray = readonlyArray as number[]; // 使用类型断言将readonlyArray重写就可以赋值成功
接口的可索引属性和额外的属性检查
可索引属性或者称为可索引类型,这是用于描述那些通过索引类型得到的类型,比如a[10]或ageMap["chenkun"]。可索引属性具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值的类型。
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
上面列子定义了StringArray接口,它具有索引签名。接口上表达了接口index索引的类型是number,返回值的类型定义的是string。其意义和Array的类型相同,数组的索引值天然就是数组类型。而对象其实就是字符串类型。索引签名类型只支持两种:字符串和数字。可以同时使用两种类型的索引,但是数字索引返回值必须是字符串索引返回值类型的子类型。这是因为当使用number来索引时,JavaScript会将它转换成string然后再去索引对象,也就是说用100(number)去索引时等同于使用“100”(string)去索引,因此两者需要保持一致。
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// 使用'string'索引,有时会得到Animal,比如使用100(number)索引也是string的索引
// 这个时候索引定义的返回类型发生冲突了,所以会定义失败
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
字符串索引能够很好描述dictionary模式,并且它们也会确保所有属性与其返回类型相匹配。因为字符串索引声明了obj.property和obj["property"]两种形式都可以。
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是字符串类型索引,索引返回类型是number
name: string // 错误,name的字符串类型索引,索引返回类型和name返回类型不匹配
}
interface NumberDictionary {
[index: number]: string;
length: number; // 可以,索引属性类型是number返回类型string,和length字符串类型索引返回类型number不冲突
1: number // 错误,1是数字索引和可索引类型都是number,但是返回类型不匹配
}
额外的属性检查:下面这个例子中,createSquare函数的参数是接口SquareConfig,但是接口上两个属性都是可选的。我们在调用函数时传入了{ colour: "black" }这个对象,此时写法上看其实没有啥问题,传入参数是createSquare接口,color和width都是不必选,而我们只是传入了一个额外属性colour,却提示报错了。
我感觉是因为使用字面量方式创建对象并且直接传入函数参数时,字面对象自动去匹配SquareConfig接口,但是{ colour: "black" }是不满足SquareConfig接口的,所以提示报错,因为它们报错的提示信息都是一致的。
有两个办法跳过这种类型的验证,一个是创建对象但是不定义SquareConfig接口类型,可以传给参数不会报错。二是使用类型推断直接将字面量对象重新定义到正确的接口类型。
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: "white", area: 100 };
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let obj: SquareConfig = {
colour: "black", // 错误,没有这个属性
}
let mySquare1 = createSquare({ colour: "black" }); // 错误,没有这个属性
// 解决办法一,创建对象但是不定义接口
let obj1 = {
colour: "black",
}
let mySquare2 = createSquare(obj1); // 错误,没有这个属性
// 解决办法二,使用as类型推断
let mySquare3 = createSquare({ colour: "black" } as SquareConfig);
当然我们学习可索引属性后,其实发现这里最优解决方案其实就是,给SquareConfig接口加一个可索引属性。
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
需要注意的是,在上面这样结构并不复杂的代码里,我们不应该去绕开这些检查。对呀包含方法和内部状态的复杂对象字面量来讲,我才应该使用这些技巧,但是额外属性检查就是在检查真正的bug。所以我们其实应该去审查类型声明,这里我们就应该检查是colour属性错误,还是SquareConfig接口上还需要增加新的属性。
函数类型
上述对接口使用让我有一种接口其实是在描述对象中的类型定义的错觉,虽然接口实际上确实能够描述JavaScript中对象拥有的各种各样的外形。但是接口也可以用来描述函数类型。
为了体现接口也表示函数类型结构,我们需要给接口定义一个调用签名。它就像是一个只有参数列表和返回值类型的函数定义。参数列表里每个参数都需要明确名字和类型。这样定义以后,我们可以像变量使用接口一样使用它。
interface SearchFunction {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunction;
mySearch = function (source, subString) {
let result = source.search(subString);
return result > -1;
}
对于函数来说,参数名不需要和接口定义的名字相匹配。即使不同参数名字也不会报错。函数的参数会逐个进行检查,要求对应位置上参数类型是兼容的。如果我不指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给了SearchFunction类型接口。而函数的返回值类型也是通过其返回值推断出来的。如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与SearchFunction定义的不匹配。