简介
TS核心原则之一是类型检查时关注数据所具有的“形状”。我们称之为鸭子类型duck typing
或者结构子类型structural subtyping
,在TS中接口充当命名这些类型的角色,是在代码中或者项目外定义代码约束的强大方式。
鸭子类型
duck typing
不在乎类型的真正实体,只要它的行为具备duck的特性,我们就可以把它当作一只duck。在动态语言中我们可以理解为,一个对象具备某种类型的特性,我们便认为这个对象是此类型的实例,而不在于它是否真的进行了实现或继承。
结构子类型
structural subtyping
在结构类型中,如果B类型的每个特征在A类型中都存在一个对应且相同的特征,则认为B类型与A类型兼容。如果两个类型相互兼容我们认为A和B相同
例子
function logMessage(data: { message: string }) {
console.log(`LOG: ${data.message}`);
}
let data = {message: "Hello Typescript", code: 200};
logMessage(data); // LOG: Hello Typescript
我们可以看到类型检查器会检查对
logMessage
的调用,logMessage
有一个参数,要求传入的对象具有一个名为message
类型为string
的属性。我们发现在实际调用logMessage
时我们的入参存在更多的属性,但是编译器只检查了需要的属性和对应的类型。当然有些情况TS的检查并不会如此宽松,我们会稍后讨论。
我们用接口描述上述的参数类型
interface logType {
message: string
}
function logMessage(data: logType) {
console.log(`LOG: ${data.message}`);
}
let data = {message: "Hello Typescript", code: 200};
logMessage(data);
我们可以看到只要数据满足interface
列出的要求便可以编译通过
可选属性
并非接口所有参数都是必传的,我们可以使用key?: type
这种方式描述可选属性。用来告诉用户参数可能存在的属性和属性类型。
interface SquareConfig {
width: number;
color?: string;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: "white", area: 100 };
newSquare.area = config.width * config.width;
if (config.color) {
newSquare.color = config.color;
}
return newSquare;
}
let squareInstance = createSquare({ width: 20 });
只读属性 readonly
属性只在首次创建时可以更改,可以在属性名前加上readonly
描述属性为只读。
interface Point {
readonly x: number;
readonly y: number;
}
let point: Point = { x: 1, y: 1 };
point.x = 2; // Error: Cannot assign to 'x' because it is a read-only property.
只读数组 ReadonlyArray
ReadonlyArray<T>
类型相对于Array<T>
移除了所有操作数组的方法,所以要确保在创建后不更改数组
let numberList: Array<number> = [1, 2, 3, 4];
let readonlyArray: ReadonlyArray<number> = numberList;
readonlyArray[0] = 2; // 不允许索引更改
readonlyArray.length = 0; // length为只读属性
readonlyArray.push(2); // 不存在push方法
numberList = readonlyArray; // 只读类型不能分配给可变类型
readonlyArray = []; // success
numberList = readonlyArray as number[]; // 只读数组可以通过类型断言赋值给普通数组
readonly
和const
使用场景
readonly
使用在属性上 const
使用在变量上
额外属性检查
当把对象字面量
赋值给变量或者作为参数传递时会进行额外属性检查,如果对象字面量
具有目标类型不具有的属性就会出现错误。
interface squareConfig {
color: string;
}
function createSquare(config: squareConfig) {}
createSquare({ color: "red", width: 100 }); // Error
绕过上述额外属性检查有几种方法:
1、类型断言
interface squareConfig {
color: string;
}
function createSquare(config: squareConfig) {}
createSquare({ color: "red", width: 100 } as squareConfig);
2、将对象分配给另一个变量
(1)、变量必须具备赋值变量的全部必传属性
(2)、变量至少存在赋值变量中的一个属性
interface squareConfig {
color: string;
width: number;
height?: number;
}
function createSquare(config: squareConfig) {}
let config = { color: "red", width: 100 };
createSquare(config);
3、字符串索引签名
interface squareConfig {
color: string;
[propName: string]: any;
}
function createSquare(config: squareConfig) {}
createSquare({ color: "red", width: 100 });
使用场景:在确认对象可以有一些以某种特殊方式使用的属性。
ATT:虽然可以多种方式绕过额外属性检查,但是在实际开发中如果真的存在额外属性检查失败大多情况属于错误,首先要考虑的是如何解决问题。
函数类型
给出参数列表和返回类型的函数声明,参数列表中的每个参数都需要名称和类型。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let myFunc: SearchFunc;
myFunc = function (source: string, subString: string): boolean {
return source.search(subString) > -1;
};
// 形参名称可以不必一致
myFunc = function (src: string, str: string): boolean {
return src.search(str) > -1;
};
// 不显式指定参数类型和返回值类型,通过TS上下文进行类型推断
myFunc = function (src, str) {
return src.search(str) > -1;
};
可索引类型
用来描述可以通过索引的类型,如a[10]
a["detail"]
。可索引类型具有一个索引签名它描述了对象索引的类型和对象索引返回值的类型。
// 下面的实例为:一个具有索引签名的接口,索引为number类型,索引返回值为string类型
interface StringArray {
[index: number]: string;
}
let arr: StringArray = ["Hello", "World"];
let first: string = arr[0];
TS支持两种索引签名:字符串、数字。可以同时使用两种索引签名,但是必须满足数字索引的返回值必须是字符串索引返回值的子类型,因为当使用数字索引时TS会把数字转成字符串进行索引,例如使用100索引时其实是通过"100"进行索引的。
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
interface MultiIndexing {
[x: number]: Dog;
[x: string]: Animal;
}
// 错误示例 1
interface MultiIndexing {
[x: number]: Animal; // Error 数字索引类型“Animal”不能赋给字符串索引类型“Dog”
[x: string]: Dog;
}
// 错误示例 2
interface NameDirectory {
[index: string]: number;
length: number;
name: string; // 类型“string”的属性“name”不能赋给字符串索引类型“number”。
}
索引签名设为只读
interface ReadonlyIndexing {
readonly [index: number]: string;
}
let arr: ReadonlyIndexing = ["Hello", "World"];
arr[0] = ""; // Error ReadonlyIndexing类型索引只允许读取
类类型
实现接口implements
用来描述一个类符合某种约束
interface User {
name: string;
id: string;
age: number;
}
class GameUser implements User {
name: string;
id: string;
age: number;
constructor() {
this.name = "Jason";
this.id = "123456";
this.age = 24;
}
}
使用接口描述一个方法
interface User {
eat(food: string): void;
}
class GameUser implements User {
name: string = "Jason";
eat(food: string): void {
console.log(`Eat Food ${food}`);
}
run() {
console.log(`${this.name} is Running!`);
}
}
// 接口User描述了GameUser的公共部分而非公共和私有两部分 并不会检查类具有某些私有成员
类静态部分和实例部分的区别
类是有两种类型的:静态部分的类型和实例部分的类型。
当类实现一个接口时,只对其实例部分进行类型检查,不会对静态部分进行类型检查。如果定义一个描述构造器的接口,并且定义一个类实现该接口时会得到一个错误。
interface ClockInterface {
new (hour: number, minute: number): any;
}
class Clock implements ClockInterface {
constructor(hour: number, minute: number) {}
}
我们可以手动检查
class
是否符合指定的构造器签名
interface ClockInterface {
new (hour: number, minute: number): any;
}
// 检测构造器签名
let Clock: ClockInterface = class Clock {
constructor(hour: number, minute: number) {}
};
一个检查静态部分签名和示例部分签名的例子
// 实例部分签名
interface ClockInterface {
createTime(): void;
}
// 静态部分签名
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
// 实例部分签名检查
class Clock implements ClockInterface {
constructor(hour: number, minute: number) {}
createTime() {}
};
// 静态部分签名检查
let clock: ClockConstructor = Clock;
接口继承接口
和类一样接口可以通过继承复制成员到另一个接口里,这样我们可以灵活的分割接口到可重用的模块中
interface Shape {
width: number;
height: number;
}
interface Position {
top: number;
bottom: number;
left: number;
right: number;
}
interface Styles extends Shape, Position {
opacity: number;
}
let eleStyles: Styles = <Styles>{};
eleStyles.width = 50;
eleStyles.height = 30;
eleStyles.top = 1;
eleStyles.bottom = 2;
eleStyles.left = 3;
eleStyles.right = 4;
eleStyles.opacity = 0.3;
// eleStyles.color = "red"; // Error 类型“Styles”上不存在属性“color”。
混合类型
接口用来描述上面提到的多种类型
// 描述一个函数,其索引的值类型为string
interface Counter {
(start: number): void; // 函数类型
[index: string]: string; // 可索引类型
}
let counter: Counter = <Counter>function (start) {};
counter.start = "1";
counter.end = "100";
counter.interval = "10";
接口继承类
当接口继承类类型时会继承类的成员但不包括其实现。
class User {
name: string = "Jason";
}
interface UserAction extends User {
run(): void;
}
class GameUser implements UserAction {
name: string = "Jhon";
run(): void {}
}
接口同样会继承类的private
protected
成员,当出现此条件时此接口只能被此类的或者此类对应的子类进行实现。
class User {
private name: string = "Jason";
}
interface UserAction extends User {
run(): void;
}
// 错误示例
class GameUser implements UserAction {
private name: string = "Jhon";
run(): void {}
}
// 修正
class GameUser extends User implements UserAction {
private name: string = "Jhon";
run(): void {}
}
class User {
private name: string = "Jason";
protected height: number = 178;
protected age: number = 25;
}
interface UserAction extends User {
run(): void;
}
// 私有属性只能在只能在基类中使用和访问 不能被重写
// 受保护的属性可以重写,只能在基类和其子类进行访问
// 受保护的属性重写可以修改修饰符为public不能修改为private
class GameUser extends User implements UserAction {
// private name: string = "Jhon";
protected height: number = 180;
public age: number = 26;
run(): void {}
constructor() {
super();
// console.log(this.name); // Error name为私有属性只能在基类中使用
console.log(this.height);
console.log(this.age);
}
}
const gamer: GameUser = new GameUser();
gamer.age = 27;
// gamer.height = 177; // Error 属性“height”受保护,只能在类“GameUser”及其子类中访问。