声明:本学习系列参考了TypeScript3.3英文版官网教程
接口(interface)
typescript的核心原则之一就是基于值的类型检查,称为“鸭式辨形”或者“结构子类型”,在typescript中,接口充当了定义这些类型的角色。
1、鸭式辨形
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeldObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
在这个例子中printLabel
函数接受一个具有一个string
类型的label
属性的对象参数,注意我们传入的对象参数不止一个label
属性,但编译器只会检查必须的属性存在且类型正确即可。我们也没有像其他语言一样要求传入的对象必须要实现这个接口,只要对象传入符合所列出的属性,那它就是允许的,这就是鸭式辨形,只要像鸭子一样走路,游泳我们就认为它就是一个鸭子。
2、可选参数
并不是接口中所有的参数都是必填的,我们可以在属性名称后面加?
来定义那些可选的参数。
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"});
使用可选属性的优势在于:我们在描述那些可选属性的同时,如果使用了接口中不存在的属性就会报错,例如我们把上例中的color
写成了clor
。
if (config.clor) {
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.clor;
}
3、只读属性
有些属性只有在初始化的时候可以修改,之后就不能改变了,这种属性叫做只读属性,在属性名前加readonly
关键字即可
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
对于只读数组也有一个ReadonlyArray<T>
的类型,用这种方式创建的数组相当于被冻结了。
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
在最后一行可以看到,甚至把它恢复成正常的数组都是非法的。
readonly vs const
当你使定义变量的时候请使用const,定义属性的时候使用readonly。
4、额外属性检查
在第一个例子中我们多传了一个参数{ size: number; label: string; }
,接着我们又学习了可选参数,如果把这两者结合起来那么你就会搬起石头砸自己的脚,例如
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });
你可能会觉得这里传入的colour
是多余参数,而 color
是可选参数所以可以不传入,那么类型检查器只要看width
有没有传入即可。看似这有些道理,但typescript认为这可能在代码中引入bug,对象字面量在typescript被特别对待,会检查多余的属性,如果对象字面量存在指定类型中未存在的属性,那么就会报错。
我们可以使用类型断言来解决这个问题
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
然而更好的方式是可以添加一个string类型的签名属性,这样SquareConfig
就可以拥有任意多的其他属性了。
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
最后一种跳过这个类型检查的方式可能会让人吃惊,只要把对象字面量赋值给一个变量即可,这样编译器就不会报错了。
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
值得注意的是,在很多情况下,特别是复杂对象的时候,你不应该去避免这种检查,如果额外属性检查出现了错误,那么往往代表你的代码可能出现了一些bug,例如你把上例的color
错写成了clor
;
5、函数类型
接口不仅可以描述对象类型,还可以描述函数类型。为了描述函数类型,我们会对函数定义一个函数签名:包括参数列表和返回值类型。每一个参数都包括了名称和类型。
interface SearchFunc {
(source: string, subString: string): boolean;
}
接着我们就可以使用这个函数类型的接口
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
对于函数类型,参数列表的参数名称不一定要相同,只要传入的顺序和类型相同即可,例如上例可以改为
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
你也可以不显示的给出参数和返回值的类型,编译器会自动推测出这些类型,如下列如果你返回一个string
类型编译器就会报错
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
6、索引(index)类型
索引类型需要有一个索引签名来描述索引的类型,索引签名支持number
和string
两种类型
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
值得注意的是JavaScript查找数字属性的时候会把数字转化为字符串,100
和 '100’是一样的。
class Animal {
name: string;
}
class 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;
[x: string]: Dog;
}
索引签名会强制所有的属性匹配它的返回值类型,这是因为字符串索引object.property
和obj["property"]
是一样的。
interface NumberDictionary {
[index: string]: number;
length: number; // ok, length is a number
name: string; // error, the type of 'name' is not a subtype of the indexer
}
7、Class类型
实现接口
像java和c#一样,Typescript也可以继承接口并且必须实现接口中的方法。
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
类的静态部分和实例部分
当在处理类和接口时,必须明白类的属性有两种类型:静态部分和实例部分。如果你创建一个具有构造器签名的接口,然后又创建一个类去实现这个接口,那么你会得到一个错误。
interface ClockConstructor {
new (hour: number, minute: number);
}
//error
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
这是因为一个类在实现一个接口的时候,仅仅是接口的实例部分被检查,而构造函数属于静态部分,它是不会被检查。
因此我们应该直接操作类的静态部分,明确指出它构造函数的返回类型。
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
8、接口继承
一个接口可继承一个或者多个接口,这可以让你把你的接口分割成更多可重复使用的组件。
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
9、混合类型
接口可以描述丰富的类型,你可能偶尔会遇到一些包含几种类型的对象,例如以下一个对象可以同时做为函数和对象使用,并带有额外的属性。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
当你和第三方库交互的时候你可能会使用这种模式去完全的描述特定的类型。
10、接口继承类
当一个接口继承一个类的时候,他会继承它所有的成员,包括private和protected成员,但是没有继承他们的实现。这意味着当你继承一个包含private和protected成员的类时,只有这个类的子类才能实现这个接口,因为其他的类都没有这个类的private和protected成员。
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
select() { }
}
// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
select() { }
}
class Location {
}
如上例只有Button
和TextBox
可以实现这个接口,而Image
和Location
因为没有Control
的的state
私有属性,而不能实现这个接口