接口
接口既可以在面向对象编程中表示为行为的抽象,也可以用来描述对象的形状。
我们用 interface 关键字来定义接口,在接口中可以用分号或者逗号分割每一项,也可以什么都不加。
1. 对象的形状
//接口可以用来描述`对象的形状`
//接口可以用来描述`对象的形状`
interface Speakable {
speak(): void;
readonly lng: string; //readonly表示只读属性 后续不可以更改
name?: string; //?表示可选属性
}
let speakman: Speakable = {
// speak() {}, //少属性会报错
name: "hello",
lng: "en",
age: 111, //多属性也会报错
};
2. 行为的抽象
接口可以把一些类中共有的属性和方法抽象出来,可以用来约束实现此接口的类
一个类可以实现多个接口,一个接口也可以被多个类实现
我们用 implements关键字来代表 实现
//接口可以在面向对象编程中表示为行为的抽象
interface Speakable {
speak(): void;
}
interface Eatable {
eat(): void;
}
//一个类可以实现多个接口
class Person implements Speakable, Eatable {
speak() {
console.log("Person说话");
}
// eat() {} //需要实现的接口包含eat方法 不实现会报错
}
3. 定义任意属性
如果我们在定义接口的时候无法预先知道有哪些属性的时候,可以使用 [propName:string]:any,propName 名字是任意的
interface Person {
id: number;
name: string;
[propName: string]: any;
}
let p1 = {
id: 1,
name: "hello",
age: 10,
};
这个接口表示 必须要有 id 和 name 这两个字段 然后还可以新加其余的未知字段
4. 接口的继承
我们除了类可以继承 接口也可以继承 同样的使用 extends关键字
interface Speakable {
speak(): void;
}
interface SpeakChinese extends Speakable {
speakChinese(): void;
}
class Person implements SpeakChinese {
speak() {
console.log("Person");
}
speakChinese() {
console.log("speakChinese");
}
}
5. 函数类型接口
可以用接口来定义函数类型
interface discount {
(price: number): number;
}
let cost: discount = function (price: number): number {
return price * 0.8;
};
6. 构造函数的类型接口
使用特殊的 new()关键字来描述类的构造函数类型
class Animal {
constructor(public name: string) {}
}
//不加new是修饰函数的,加new是修饰类的
interface WithNameClass {
new (name: string): Animal;
}
function createAnimal(clazz: WithNameClass, name: string) {
return new clazz(name);
}
let a = createAnimal(Animal, "hello");
console.log(a.name);
其实这样的用法一般出现在 当我们需要把一个类作为参数的时候 我们需要对传入的类的构造函数类型进行约束 所以需要使用 new 关键字代表是类的构造函数类型 用以和普通函数进行区分
接口和类型别名的区别
实际上,在大多数的情况下使用接口类型和类型别名的效果等价,但是在某些特定的场景下这两者还是存在很大区别。
1.基础数据类型 与接口不同,类型别名还可以用于其他类型,如基本类型(原始值)、联合类型、元组
// primitive
type Name = string;
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
// dom
let div = document.createElement("div");
type B = typeof div;
2.重复定义
接口可以定义多次 会被自动合并为单个接口 类型别名不可以重复定义
interface Point {
x: number;
}
interface Point {
y: number;
}
const point: Point = { x: 1, y: 2 };
3.扩展 接口可以扩展类型别名,同理,类型别名也可以扩展接口。但是两者实现扩展的方式不同
接口的扩展就是继承,通过 extends 来实现。类型别名的扩展就是交叉类型,通过 & 来实现。
// 接口扩展接口
interface PointX {
x: number;
}
interface Point extends PointX {
y: number;
}
// ----
// 类型别名扩展类型别名
type PointX = {
x: number;
};
type Point = PointX & {
y: number;
};
// ----
// 接口扩展类型别名
type PointX = {
x: number;
};
interface Point extends PointX {
y: number;
}
// ----
// 类型别名扩展接口
interface PointX {
x: number;
}
type Point = PointX & {
y: number;
};
4.实现 这里有一个特殊情况 类无法实现定义了联合类型的类型别名
type PartialPoint = { x: number } | { y: number };
// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint {
// Error
x = 1;
y = 2;
}
泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
为了更好的了解泛型的作用,我们可以看下面的一个例子
function createArray(length: number, value: any): any[] {
let result = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, "x"); // ['x', 'x', 'x']
上述这段代码用来生成一个长度为 length,值为 value 的数组 但是我们其实可以发现一个问题 不管我们传入什么类型的 value,返回值的数组永远是 any 类型。
如果我们想要的效果是:我们预先不知道会传入什么类型,但是我们希望不管我们传入什么类型,我们的返回的数组的值里面的类型应该和参数保持一致,那么这时候泛型就登场了。
使用泛型改造
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray<string>(3, "x"); // ['x', 'x', 'x']
我们可以使用 <> 的写法,然后再面传入一个变量 T 用来表示后续函数需要用到的类型,当我们真正去调用函数的时候再传入 T 的类型就可以解决很多预先无法确定类型相关的问题。
1. 多个类型参数
如果我们需要有多个未知的类型占位,那么我们可以定义任何字母来表示不同的类型参数。
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
swap([7, "seven"]); // ['seven', 7]
2. 泛型约束
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
return arg;
}
// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.
上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。
这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
注意:我们在泛型里面使用 extends 关键字代表的是泛型约束,需要和类的继承区分开。
3. 泛型接口
定义接口的时候也可以指定泛型
interface Cart<T> {
list: T[];
}
let cart: Cart<{ name: string; price: number }> = {
list: [{ name: "hello", price: 10 }],
};
console.log(cart.list[0].name, cart.list[0].price);
我们定义了接口传入的类型 T,之后返回的对象数组里面 T 就是当时传入的参数类型。
4 泛型类
class MyArray<T> {
private list: T[] = [];
add(value: T) {
this.list.push(value);
}
getMax(): T {
let result = this.list[0];
for (let i = 0; i < this.list.length; i++) {
if (this.list[i] > result) {
result = this.list[i];
}
}
return result;
}
}
let arr = new MyArray();
arr.add(1);
arr.add(2);
arr.add(3);
let ret = arr.getMax();
console.log(ret);
上述例子我们实现了一个在数组里面添加数字并且获取最大值的泛型类。
5. 泛型类型别名
type Cart<T> = { list: T[] } | T[];
let c1: Cart<string> = { list: ["1"] };
let c2: Cart<number> = [1];
6. 泛型参数的默认类型
我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}