目录
前言
一、认识接口
二、可选属性
三、只读属性
四、函数类型
五、可索引的类型
六、类类型
1、实现接口
2、类静态部分与实例部分的区别
七、继承接口
八、混合类型
九、接口继承类
TypeScript的核心原则之一是对值所具有的结构进行类型检查,接口的作用就是为类型命名和你的代码或者第三方代码定义契约。
interface Point {
x: number;
y: number;
}
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 100, y: 100 });
就像我们上面使用类型别名时一样,这个示例的工作方式就像我们使用了匿名对象类型一样。TypeScript 只关心我们传递给 printCoord 的值的结构——它只关心它是否具有预期的属性。仅仅关注类型的结构和功能,这就是为什么我们称TypeScript为结构类型系统。
类型别名和接口非常相似,在许多情况下,您可以在它们之间自由选择。接口的几乎所有特性都是类型可用的,关键区别在于不能重新打开类型以添加新的属性,而接口总是可扩展的。
Interface |
Type |
---|---|
Extending an interface 扩展接口
|
Extending a type via intersections 通过交叉点扩展类型
|
Adding new fields to an existing interface 向现有接口添加新字段 |
A type cannot be changed after being created 类型创建后不能更改 |
接口里的属性不全都是必需的。有些只是在某些条件下存在,或者根本不存在。可选属性在应用"option bags"模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。
下面是应用了“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"});
带有可选属性的接口与普通的接口定义差不多,只是可选属性名字定义的后面加一个?符号。
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。比如,我们故意将createSquare里的color属性名拼错,就会得到一个错误提示:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = {color: "white", area: 100};
if (config.clor) {
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.clor;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
一些对象属性只能在对象刚刚创建的时候修改其值,你可以在属性名前用readonly来指定只读属性:
interface Point{
readonly x: number;
readonly y: number;
}
你可以通过赋值一个对象 初始值来构造一个Point,赋值以后,x和y再也不能改变了。
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
TypeScript具有ReadONLYArray
let a : number[] = [1,2,3,4];
let ro : ReadonlyArray = a;
ro[0] =12; //error!
ro.push(5);//error!
ro.length=100;//error
a = ro;//error
a=ro,就算把整个ReadonlyArray赋值到一个普通的数组都不行,但是可以用类型断言重写:
a = ro as number[];
readonly vs const
最简单判断:作为变量的话用const,作为属性的话用readonly。
接口可以描述对象拥有的外形,除了描述带有属性的普通对象外,接口也可以描述函数类型。
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。它就 像是一个只有参数列表和返回值类型的函数定义,参数列表里的每个参数都需要名字和类型。
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;
}
函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的,如果你不想指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给SearchFunc类型变量。函数的返回值类型是通过其返回值推断出来的,如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与SearchFunc接口中的定义不匹配。
let mySearch: SearchFunc;
mySearch = function(src,sub){
let result = src.search(sub);
return result> -1;
}
与接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如a[10]或b["sde"],可索引类型具有一个索引签名,他描述了对象索引的类型,还有相应索引返回值的类型。
interface StringArray{
[index: number]:string;
}
let myArray:StringArray;
myArray = ["Bob","Fred"];
let myStr: string = myArray[0];
上面例子里,我们定义了StringArray
接口,它具有索引签名。 这个索引签名表示了当用 number
去索引StringArray
时会得到string
类型的返回值。
TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number
来索引时,JavaScript会将它转换成string
然后再去索引对象。 也就是说用 100
(一个number
)去索引等同于使用"100"
(一个string
)去索引,因此两者需要保持一致。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
字符串索引签名能够很好的描述dictionary
模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.property
和obj["property"]
两种形式都可以。 下面的例子里, name
的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
}
最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
你不能设置myArray[2]
,因为索引签名是只读的。
TypeScript能够用它来明确的强制一个类去符合某种契约。
interface ClockInterface{
currentTime: Date;
}
class Clock implements ClockInterface{
currentTime:Date;
constructor(h:number,m:number){}
}
也可以在接口中实现一个方法,在类里实现它,如同下面的setTime方法一样:
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 Alarm {
alert(): void;
}
class Door {
}
class SecurityDoor extends Door implements Alarm {
alert() {
console.log('SecurityDoor alert');
}
}
class Car implements Alarm {
alert() {
console.log('Car alert');
}
}
一个类可以实现多个接口:
interface Alarm {
alert(): void;
}
interface Light {
lightOn(): void;
lightOff(): void;
}
class Car implements Alarm, Light {
alert() {
console.log('Car alert');
}
lightOn() {
console.log('Car light on');
}
lightOff() {
console.log('Car light off');
}
}
上例中,Car
实现了 Alarm
和 Light
接口,既能报警,也能开关车灯。
类具有两个类型,静态部分和实例部分,如果你用构造签名去定义一个接口并试图定义一个类去实现这个接口会出错:
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
因为,当一个类实现了一个接口,只对其实例部分进行类型检查。contructor存在于类的静态部分,所以不在检查范围内。
因此我们应该直接操作类的静态部分,举例,我们定义两个接口,ClockConstructor为构造函数所用和ClockInterface为实例方法所用。为了方便我们定义一个构造函数createClock,它用传入的类型创建实例。
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 DigtalClock 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);
因为createClock
的第一个参数是ClockConstructor
类型,在createClock(AnalogClock, 7, 32)
里,会检查AnalogClock
是否符合构造函数签名。
和类一样,接口也可以相互继承。让我们能够从一个接口里赋值成员到另一个接口。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = {};
square.color = "blue";
square.sideLength = 10;
可实现多继承,创建继承多个接口的合成接口:
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = {};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
混合类型:一个对象可以同时做为函数和对象使用,并带有额外的属性。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
当接口继承一个类的类型时,它会继承类的成员但不包括实现,当你创建一个接口继承一个拥有私有或者保护成员的类的时候,这个接口类型只能被这个类或者其子类实现。
接口继承类:当你有一个庞大的继承结构时这很有用,但要指出你的代码只在子类拥有特定属性时起作用。 这个子类除了继承至基类外与基类没有任何关系。 例:
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
select() { }
}
// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl {
select() { }
}
class Location {
}
在上面的例子里,SelectableControl
包含了Control
的所有成员,包括私有成员state
。 因为 state
是私有成员,所以只能够是Control
的子类们才能实现SelectableControl
接口。 因为只有 Control
的子类才能够拥有一个声明于Control
的私有成员state
,这对私有成员的兼容性是必需的。
在Control
类内部,是允许通过SelectableControl
的实例来访问私有成员state
的。 实际上, SelectableControl
接口和拥有select
方法的Control
类是一样的。 Button
和TextBox
类是SelectableControl
的子类(因为它们都继承自Control
并有select
方法),但Image
和Location
类并不是这样的。