接口、枚举、泛型
接口的声明
在前面我们通过type可以用来声明一个对象类型,对象的另外一种声明方式就是通过接口来声明:
准确说 ts 中的接口是接口类型,这和 Java 中的接口不一样。Java 中的接口是一个类,可以直接在类中定义方法属性;但是 ts 中的接口是个类型,类型自然不能独立使用,它需要和类一起使用,用来约束类或者和其他的什么一起使用。所以说 interface 的作用,其实定义类型别名的 type 也基本能做到。因为他们都是在定义类型。
// 通过类型(type)别名来声明对象类型// type InfoType = {name: string, age: number}// 另外一种方式声明对象类型: 接口interface// 在其中可以定义可选类型// 也可以定义只读属性interface IInfoType {readonly name: string // 只读属性age: numberfriend?: { // 可选属性name: string}}const info: IInfoType = {name: "why",age: 18,friend: {name: "kobe"}}console.log(info.friend?.name)console.log(info.name)// info.name = "123"info.age = 20
索引类型
索引就是对象中的 key,给对象中的属性名也进行类型限制
// 通过interface来定义索引类型interface IndexLanguage {[index: number]: string}const frontLanguage: IndexLanguage = {0: "HTML",1: "CSS",2: "JavaScript",3: "Vue"}interface ILanguageYear {[name: string]: number}const languageYear: ILanguageYear = {"C": 1972,"Java": 1995,"JavaScript": 1996,"TypeScript": 2014}
函数类型
前面我们都是通过 interface 来定义对象中普通的属性和方法的,实际上它也可以和 type 一样用来定义函数类型:
当然,除非特别的情况,还是推荐大家使用类型别名来定义函数:
// type CalcFn = (n1: number, n2: number) => number// 可调用的接口interface CalcFn {(n1: number, n2: number): number // 函数类型}function calc(num1: number, num2: number, calcFn: CalcFn) {return calcFn(num1, num2)}const add: CalcFn = (num1, num2) => {return num1 + num2}calc(20, 30, add)
接口继承
接口和类一样是可以进行继承的,也是使用extends关键字,并且接口是支持多继承的(类不支持多继承)
interface ISwim {swimming: () => void}interface IFly {flying: () => void}interface IAction extends ISwim, IFly {}const action: IAction = {swimming() {},flying() {}}
接口的实现
接口定义后,可以被类实现。面向接口编程就是只看接口中定义了什么 api,具体的实现不用管,直接使用接口中的东西就行。具体执行的时候就把实现类的实例传入。
接口就好比特殊的抽象类,可以多继承。实现implement也就是特殊的继承。
interface ISwim {swimming: () => void}interface IEat {eating: () => void}// 类实现接口class Animal {}// 继承: 只能实现单继承// 实现: 实现接口, 类可以实现多个接口class Fish extends Animal implements ISwim, IEat {swimming() {console.log("Fish Swmming")}eating() {console.log("Fish Eating")}}class Person implements ISwim {swimming() {console.log("Person Swimming")}}// 面向接口编程:将接口传入即可function swimAction(swimable: ISwim) {swimable.swimming()}// 1.所有实现了接口的类对应的对象, 都是可以传入swimAction(new Fish())swimAction(new Person())swimAction({swimming: function() {}})
交叉类型
交叉类型(Intersection Types):交叉类似表示需要满足多个类型的条件,交叉类型使用 &符号;
比如type hhh = string & number,hhh 类型的值需要同时符合 string 类型和 number 类型,没有这样的值,所以其实是个 never 类型。
基本类型基本无法交叉,所以,在开发中,我们进行交叉时,通常是对对象类型进行交叉。
// 一种组合类型的方式: 联合类型type WhyType = number | stringtype Direction = "left" | "right" | "center"// 另一种组件类型的方式: 交叉类型type WType = number & stringinterface ISwim {swimming: () => void}interface IFly {flying: () => void}type MyType1 = ISwim | IFlytype MyType2 = ISwim & IFlyconst obj1: MyType1 = {flying() {}}const obj2: MyType2 = {swimming() {},flying() {}}
interface 和 type 区别
我们会发现interface和type都可以用来定义对象类型,那么在开发中定义对象类型时,到底选择哪一个呢?
- 如果是定义非对象类型,通常推荐使用type,比如Direction、Alignment、一些Function;
如果是定义对象类型,那么他们是有区别的:
- interface 可以重复的对某个接口来定义属性和方法;
- 而 type 定义的是别名,别名是不能重复的;
因为可以重名,所以有合并的效果,比如给一些默认的对象添加属性。
// window math 等这些默认对象存在 ts 的库文件中interface Window {age: number // 相当于合并了 age 属性到 window 中}window.age = 19console.log(window.age)
字面量赋值
看下面代码
interface IPerson {name: stringage: numberheight: number}const info = {name: "why",age: 18,height: 1.88,address: "广州市"}// 直接字面量赋值会报错const t: IPerson = {name: "why",age: 18,height: 1.88,address: "广州市" // 因为 Iperson 类型没有 address 属性}// 但是拿字面量对象的引用赋值,却不会报错const p: IPerson = info
这是因为 TypeScript 在字面量直接赋值的过程中,为了进行类型推导会进行严格的类型限制。
但是之后如果我们是将一个变量标识符赋值给其他的变量时,会进行freshness擦除操作。它会擦除掉多余的部分以满足类型校验。
使用场景:比如一个函数接收对象,有擦除操作就可以接收不仅有类型要求的属性,还有多出其他属性的对象。相当于扩大了函数接收对象的范围。并且严格限制了字面量直接传参。
function printInfo(person: IPerson) {console.log(person)}// 代码会报错,严格限制字面量直接传参printInfo({name: "why",age: 18,height: 1.88,address: "广州市"})// 传入有多余属性的对象引用,正确printInfo(info)
枚举
枚举就是定义一个类型,这个类型的取值是有限的,只有我们列出来的这几个值。和字面量类型组成联合类型很相似。
// type Direction = "left" | "Right" | "Top" | "Bottom"enum Direction {LEFT, // 因为就跟常量一样,所以一般大写RIGHT,TOP,BOTTOM}function turnDirection(direction: Direction) {switch (direction) {case Direction.LEFT:console.log("改变角色的方向向左")break;case Direction.RIGHT:console.log("改变角色的方向向右")break;case Direction.TOP:console.log("改变角色的方向向上")break;case Direction.BOTTOM:console.log("改变角色的方向向下")break;default:const foo: never = direction;break;}}turnDirection(Direction.LEFT)turnDirection(Direction.RIGHT)turnDirection(Direction.TOP)turnDirection(Direction.BOTTOM)
枚举类型的值
列出来的枚举类型的值,其实也是个变量,它的值默认是 0 开始自然递增,并且我们也可以修改它的值。比如将中间枚举值修改为 100,那它之后的枚举值的实际值就是从 101 开始递增。

泛型
类型参数化
软件工程的主要目的是构建不仅明确且一致的API,还要让你的代码具有很强的可重用性:比如我们可以通过函数来封装一些API,通过传入不同的函数参数,让函数帮助我们完成不同的操作;
为了提高通用性,对于参数的类型我们也可以进行参数化。泛型就是类型的参数化。
// 类型的参数化// 在定义这个函数时, 我不决定这些参数的类型// 而是让调用者以参数的形式告知,我这里的函数参数应该是什么类型// 可以传入多个泛型function sum<Type, E>(hhh: Type, msg: E): Type {return hhh}// 1.调用方式一: 明确的传入类型sum<number, string>(20, 'qwe')sum<{name: string}, number>({name: "why"}, 123)sum<any[]>(["abc"])// 2.调用方式二: 类型推到sum(50, 'qwe')sum("abc", {name: 'zs'})
平时在开发中我们可能会看到一些常用的名称:
- T:Type的缩写,类型
- K、V:key和value的缩写,键值对
- E:Element的缩写,元素
- O:Object的缩写,对象
泛型接口
```javascript interface IPerson{ name: T1 age: T2 }
const p: IPerson = { name: “why”, age: 18 }
<a name="gdn4y"></a>## 泛型类```javascript// 给类添加泛型class Point<T> {x: Ty: Tz: Tconstructor(x: T, y: T, z: T) {this.x = xthis.y = ythis.z = y}}// 自动推导const p1 = new Point("1.33.2", "2.22.3", "4.22.1")// 明确指定const p2 = new Point<string>("1.33.2", "2.22.3", "4.22.1")const p3: Point<string> = new Point("1.33.2", "2.22.3", "4.22.1")const names1: string[] = ["abc", "cba", "nba"]// 之前定义的数组的方式之一,其实就是类的泛型const names2: Array<string> = ["abc", "cba", "nba"] // 不推荐(react jsx <>)
泛型约束
有时候我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中:
- 比如 string 和 array 都是有 length 的,或者某些对象也是会有 length 属性的;
- 那么只要是拥有 length 的属性都可以作为我们的参数类型,那么应该如何操作呢?
我们可以使用extends关键字
interface ILength {length: number}// 泛型继承接口类型,约束了泛型,使得传入的类型必须拥有 length 属性function getLength<T extends ILength>(arg: T) {return arg.length}getLength("abc")getLength(["abc", "cba"])getLength({length: 100})
