接口、枚举、泛型
接口的声明
在前面我们通过type可以用来声明一个对象类型,对象的另外一种声明方式就是通过接口来声明:
准确说 ts 中的接口是接口类型,这和 Java 中的接口不一样。Java 中的接口是一个类,可以直接在类中定义方法属性;但是 ts 中的接口是个类型,类型自然不能独立使用,它需要和类一起使用,用来约束类或者和其他的什么一起使用。所以说 interface 的作用,其实定义类型别名的 type 也基本能做到。因为他们都是在定义类型。
// 通过类型(type)别名来声明对象类型
// type InfoType = {name: string, age: number}
// 另外一种方式声明对象类型: 接口interface
// 在其中可以定义可选类型
// 也可以定义只读属性
interface IInfoType {
readonly name: string // 只读属性
age: number
friend?: { // 可选属性
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 | string
type Direction = "left" | "right" | "center"
// 另一种组件类型的方式: 交叉类型
type WType = number & string
interface ISwim {
swimming: () => void
}
interface IFly {
flying: () => void
}
type MyType1 = ISwim | IFly
type MyType2 = ISwim & IFly
const 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 = 19
console.log(window.age)
字面量赋值
看下面代码
interface IPerson {
name: string
age: number
height: 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: T
y: T
z: T
constructor(x: T, y: T, z: T) {
this.x = x
this.y = y
this.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})