外观
Typescript中的高级类型
作者:guo-zi-xin
更新于:9 个月前
字数统计:3.1k 字
阅读时长:11 分钟
交叉类型(Intersection Types): &
交叉类型是将多个类型合并为同一个类型,这让我们可以把现有的类型叠加到一起成为一种类型, 它包含了所需的所有类型的特性。
👋 不适用于基本类型 如string
类型、number
类型
关于交叉类型产生never
类型
交叉类型在使用的时候有时候会产生一个新的类型never
, 一般产生这种情况是两个interface
使用交叉类型进行处理, 它们当中都有一个name
的同名属性,但两个类型不同,interface1
中的name
是string
类型, interface2
的name
属性是number
类型, 但没有一个属性的类型即是string
类型又是number
类型
typescript
interface IDefaultFirst {
name: string;
}
interface IDefaultSecond {
name: number;
}
type CrossType = IDefaultFirst & IDefaultSecond; // CrossType中的name的类型将为‘never’
示例
具体类型联合
typescriptinterface First { name: string; } interface Second { question: sstring; id: numbher; } const getCrossover = <First, Second>(first: First, second: Second): First & Second => { // Partial 是Typescript的工具类型 // Partial<Type> // 作用是 构造类型 Type ,并将它所有的属性设置为可选的。它的返回类型表示输入类型的所有子类型 const result: Partial<First & Second> = {} for (const key in first) { if (first.hasOwnProperty(key)) { (result as First)[key] = first[key] } } for (const keys in second) { if (second.hasOwnPropertu(keys)) { (result as Second)[keys] = seconds[keys] } } return result as First & Second // 这个类型断言可以省略 } const obj = getCrossover({ a: 'hello' }, { b: 42 }) // 现在 obj就同时拥有了 a 属性与 b 属性
泛型联合
typescriptconst getCrossover = <T extends object, U extends object>(first: T, second: U): T & U => { const result = <T & U>{} for (const key in first) { (<T>result)[key] = first[key] } for (const keys in second) { if (!result.hasOwnProperty(keys)) { (<U>result)[keys] = second[keys] } } return result } const obj = getCrossover({ a: 'hello' }, { b: 42 }) // 现在 obj就同时拥有了 a 属性与 b 属性
使用交叉类型进行接口混入
typescript// 初始时的 question 和 answer 定义 interface IQuestionRecord { createTime: string; userName: string; userAvatar: string; question: { title: string; content: string; picture: string[]; }; } interface IAnswerRecord { createTime: string; userName: string; userAvatar: string; answer: { comment: string; audio?: { url: string; }; }; } // 👇👇👇👇👇通过提取公共部分, 利用联合类型将类型定义简化并且可复用 interface IUserBaseinfo { createTime: string; userName: string; useAvatar:string; } interface IQuestionRecord { question: { title: string; content: string; picture: string[] } } interface IAnswerRecord { answer: { comment: string; audio?: { url: string } } } // 使用交叉类型混入 // Mixin 类型的含义是遍历传入的泛型 T 和 U 的所有属性, 并且把它们联合,产生新的类型对象, 新的类型对象上每个属性的类型是它们在原本类型上的属性 // Mixin<T,U> 是泛型类的写法 type Mixin<T,U> = { [P in keyof (T & U)]: (T & U)[P] } // 另一种写法 type Mixin<T, U> = T & U // 使用泛型混入, 方便之后复用 // MixinUserInfo 类型的含义是通过调用混合类型 Mixin, 先将共同的部分IUserBaseInfo类型先混入进去, 整合成新类型, 方便给具体的 question 和 answer 来调用 type MixinUserInfo<T> = Mixin<IUserBaseInfo, T> // 这里的 IRecordConfig 是把 question 和 answer 整合成一个类型, 减少不必要的类型定义 interface IRecordConfig { question?:MixinUserInfo<IQuestionRecord> answer?: MixinUserInfo<IAnswerRecord> } // 最终使用的时候的类型 export type RecordConfigList = IRecordConfig[]
联合类型(Union Types): |
联合类型与交叉类型很有关联,但是使用上完全不同。 联合类型会产生一个包含所有类型的选择集的类型,表示一个值的类型是定义的联合类型中的其中一种。
当一个变量希望传入某种类型时,可以考虑使用联合类型
当一个值是联合类型对象时, 我们只能访问这个联合类型中的所有类型中的共同成员
- 示例
定义常量
typescript
type IQuestionType = string | number | boolean
// 这里表示id的类型是布尔值 它也可以定义为字符串 数字 都是可以正常运行的
const id:IQuestionType = false
在函数中定义
typescript
const getUnoinType = (value: string, padding: string | number) => {
if (typeof padding === 'number') {
return Array(padding + 1).join('') + value
} else if (typeof padding === 'string') {
return padding + value
}
throw new Error(`Expected string or number, got '${padding}'`)
}
getUnoinType("Hello world", 4);
当一个值是联合类型对象时, 我们只能访问这个联合类型中的所有类型中的共同成员
typescript
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
const getSmallPet = ():Bird | Fish => {
// ...
}
const pet = getSmallPet();
pet.layEggs(); // 正常运行
pet.swim(); // 报错, Bird 类型中没有这个属性
类型守卫(Type Guards)
类型守卫是一种用于收窄或者断言变量的技术, 通常与联合类型与交叉类型一起使用。
类型守卫可以通过一些条件检查来确定变量的确切类型, 以便在后续的代码中使用更具体的类型信息
类型守卫通常有以下几种方式
typeof
类型守卫
使用typeof
操作符检查变量类型
typescript
const printValue = (value: string | number) => {
// 这里使用 typeof 操作符将 value 值的类型范围收窄到 string 类型, 之后就可以调用字符串的方法
if (typeof value === 'string') {
console.log(value.toUpperCase())
// else判断体里的逻辑是将 value 的类型推断为 number类型, 之后调用 Number 类型的方法
} else {
console.log(value.toFixed(0))
}
}
instanceof
类型守卫
使用instanceof
操作符检查对象是否属于某个类
typescript
class Cat {
meow() {
console.log('Meow')
}
}
class Dog {
bark() {
console.log('Bark')
}
}
const makeSound = (animial: Cat | Dog) => {
// 这里使用 instanceof 操作符将 animial 的类型收窄到 Cat 类上, 之后调用 meow() 方法
if (animial instanceof Cat) {
animial.meow()
// else 判断体的逻辑是将 animial 推断为属于 Dog 这个类型, 之后调用bark方法
} else {
animial.bark()
}
}
自定义类型守卫
通过定义一个返回类型谓词的函数, 来自定义一个类型守卫
🫸 类型谓词 的形式是 paramterName is Type
这种形式, paramterName
必须是来自当前函数签名里的一个参数名, Type
表示一个类型 🫷
typescript
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
// pet is Fish 这一段就是类型谓词
const isFish = (pet: Bird | Fish):pet is Fish => {
return (pet as Fish).swim !== undefined
}
const getSmallPet = ():Bird | Fish => {
// ...
}
const pets = getSmallPet();
// 这里 通过自定义的 类型谓词 将pets的类型收窄为 Fish, 之后调用 Fish 类型定义的操作函数
if (isFish(pets)) {
pets.swim()
// else判断体中是将 pets 类型推断为 Bird 类型, 之后调用 Bird 类型定义的操作函数
} else {
pet.fly()
}
in
操作符
in
操作符可以作为类型细化表达式来使用
对于n in x
表达式, 其中n
是字符串字面量或字符串字面量类型, 并且x
是个联合类型, 那么true
分支的类型细化为有一个可选的或者必须的属性, false
分支的类型细化为有一个可选的或不存在属性n
typescript
const move = (pet: Fish | Bird) => {
if ('swim' in pet) {
return pet.swom();
}
return pet.fly();
}
类型别名
类型别名, 顾名思义, 就是给一个类型起一个新名字, 但是不会新创建一个类型。
类型别名有时候和接口很相似, 但是可以作用于原始值、联合类型,元组预计其它任何需要手写的类型。
但是不需要给原始类型起别名,通常没什么用处, 尽管可以运行。
typescript
type Name = string
type NameResolver = () => string
type NameOrResolver = Name | NameResolver
const getUserName = (name: NameOrResolver): Name => {
if (typeof name === 'string') {
return name;
} else {
return name();
}
}
泛型式类型别名
同接口一样, 类型别名可以是泛型 - 我们可以添加参数类型并且在别名声明的右侧传入:
typescript
type Container<T> = { value: T};
// 在类型别名属性中引用自身
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
与交叉类型一起使用:
typescript
type LinkedList<T> = T & { next: linkedList<T> };
interface Person {
name: string;
}
const people: LinkedList<Person>
const name1 = people.name;
const name2 = people.next.name;
const name3 = people.next.next.name;
const name4 = people.next.next.next.name ;
TIP 🔔
类型别名不能出现在右侧任何地方。
typescripttype Yikes = Array<Yikes> // 这个写法会报错
如果需要使用类型注解的层次结构,请使用接口。 它能使用
implements
和extedns
。为一个简单的对象类型使用类型别名, 只需要一个与异化的名字就可以。 另外。 当想给联合类型和交叉类型提供一个语义化的名称时, 类型别名是一个好的选择。
请注意,类型别名在 TypeScript 中只是给现有类型起了一个别名,它们并不会创建出不同或独立的类型。当你使用类型别名时,实际上就相当于直接使用了被别名的原始类型。换句话说,类型别名并不会创建出全新的、不同的类型。
typescript
type A = { x: number };
type B = A;
let a: A = { x: 42 };
let b: B = { x: 10 };
a = b; // 合法
b = a; // 合法
上述示例中,类型别名 B 被定义为类型 A 的别名,因此变量 a 和 b 可以互相赋值,因为它们实际上都是指向相同的类型。尽管在代码中看起来好像创建了两个不同的类型,但在 TypeScript 视角下,它们实际上是完全相同的类型
接口与类型别名
类型别名虽然可以和接口一样声明, 但是它们并不同。
接口创建了一个新的名字,可以在其它任何地方使用,但类型别名并不创建新名字 ——例如, 错误信息就不会使用别名。在下面示例中,在编辑器中将鼠标悬停在
interfaceed
上, 显示它返回的是Interface
,但悬停在aliased
上时,现实的却是这个字面量类型:typescripttype Alias = { num: number }; interface interface { num: number }; declare const aliased = (arg: Alias):Alias => {} declare const interfaced = (arg: Interface): Interface => {}
在旧版本的 TypeScript 里,类型别名不能被继承和实现(它们也不能继承和实现其它类型)。从 TypeScript 2.7 开始, 类型别名可以被继承并生成新的交叉类型。例如: type Cat = Animal & { purrs: true } 。
因为软件中的对象应该对于扩展是开放的,但是对于修改是封闭的 (opens new window),你应该尽量去使用接口代替类型别名。
如果无法通过接口来描述一个类型并且需要使用联合类型或元组类型, 这个时候通常会使用类型别名
元组类型(Tuple)
用于表示固定长度和固定类型排列的数组。在元组中,每个位置上的元素都有一个确定的类型
typescript
let x: [string, number]
// 初始化
x = ['hello', 10]
// 错误的初始化
x = [10, 'hello']
但是访问一个已知的索引, 会得到正确的类型
typescript
console.log(x[0].substr(1)) // ok
console.log(x[1].substr(1)) // Error number 类型没有substr方法
元组类型在需要固定长度和类型的数组场景下非常有用,例如表示一对坐标、表示函数返回多个不同类型的值等。通过使用元组类型,可以更好地约束数组的结构,提高代码的类型安全性。
当访问超出已知索引的元素时,会返回元组包含的类型的所有联合类型
infer 关键字
表示在extends
条件语句中待推断的类型变量,它是从泛型里面进行推断
typescript
type ParamType<T> = T extends (arg: infer P) => any ? P: T
在这个条件语句中 T extends (arg: infer P) => any ? P : T
中, infer P
表示待推断的函数参数
整句的含义为: 如果T
能赋值给 (arg: infer P) => any
, 则结果是 (arg: infer P) => any
类型中的参数P
否则返回T
typescript
type shiftArr<arr extends unknown[]> = arr extends [unknown, ...infer restArr]
? restArr
: never;
type footArr = shiftArr<[1, 2, 3]>;
上面这个示例得到的结果是得到一个去掉首位字符的数组[2,3]
, 但它不是结果, 它是一个类型
整句的含义为shiftArr
类型中传入了泛型arr
, arr
是继承于(或者说arr的类型范围限制在了)unknown
数组, 我们通过数组解构的语法 将除去首位字符的元素推断成一个新类型restArr
,如果这个restArr
存在,那么就返回这个新类型,否则就返回never
类型
引用
https://jkchao.github.io/typescript-book-chinese/typings/overview.html#%E7%B1%BB%E5%9E%8B%E5%88%AB%E5%90%8D
https://bosens-china.github.io/Typescript-manual/download/zh/reference/advanced-types.html#%E7%B1%BB%E5%9E%8B%E5%AE%88%E5%8D%AB%E4%B8%8E%E7%B1%BB%E5%9E%8B%E5%8C%BA%E5%88%86-type-guards-and-differentiating-types
https://juejin.cn/post/7283797053338517545?searchId=202312121416195FE61D891B64900A0F78#heading-7