TS系列篇|高级数据类型
"不畏惧,不将就,未来的日子好好努力"——大家好!我是小芝麻😄
1、交叉类型&
通过
&
运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
- 交叉类型是将多个类型合并为一个类型。交叉类型其实就是两个接口类型的属性的并集
type PersonName = { name: string }
type Person = PersonName & { age: number }
let person: Person = {
name: '金色小芝麻',
age: 18
}
在上面代码中我们先定义了 PersonName
类型,接着使用 &
运算符创建一个新的 Person
类型,表示一个含有 name
和 age
的类型,然后定义了一个 person
类型的变量并初始化。
- 在合并多个类型的过程中,刚好出现某些类型存在相同的成员,但对应的类型又不一致时
interface X {
c: string;
d: string;
}
interface Y {
c: number;
e: string
}
type XY = X & Y;
type YX = Y & X;
let p: XY = { c: 6, d: "d", e: "e" }; // ERROR 不能将类型“number”分配给类型“never”。
let q: YX = { c: "c", d: "d", e: "e" }; // ERROR 不能将类型“string”分配给类型“never”。
在上面的代码中,接口 X 和接口 Y 都含有一个相同的成员 c,但它们的类型不一致。对于这种情况,此时 XY 类型或 YX 类型中成员 c 的类型为 string & number
,即成员 c 的类型既是 string
类型又是 number
类型。很明显这种类型是不存在的,所以混入后成员 c 的类型为 never
。[2]
2、联合类型 |
联合类型使用
|
分隔每个类型。
- 联合类型(Union Types)表示取值可以为多种类型中的一种
- 未赋值时联合类型上只能访问两种类型共有的属性和方法
let name: string | number
name = 3
name = '金色小芝麻'
这里的 let name: string | number
的含义是,允许 name
的类型是 string
或者 number
,但不能是其他类型。
name = true // ERROR 不能将类型“true”分配给类型“string | number”。
访问联合类型的属性或方法
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:
function getLength(something: string | number): number {
return something.length; // ERROR
}
上例中,
length
不是 string
和 number
的共有属性,所以会报错。
访问 string
和 number
的共有属性是没问题的:
function getString(something: string | number): string {
return something.toString();
}
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:
let name: string | number
name = 3
console.log(name.length) // ERROR 类型“number”上不存在属性“length”。
name = '金色小芝麻'
console.log(name.length) // 5
上例中,第二行的 name
被推断成了 number
,访问它的 length
属性时就报错了。
而第四行的 name
被推断成了 string
,访问它的 length
属性不会报错。
3、字面量类型
- 可以把字符串、数字、布尔值字面量组成一个联合类型
type ZType = 1 | 'one' | true
let t1: ZType = 1
let t2: ZType = 'one'
let t3: ZType = true
// 字面量类型
let Gender3: 'BOY' | 'GIRL'
Gender3 = 'BOY' // 可以编译
Gender3 = 'GIRL' // 可以编译
Gender3 = true // 编译失败
3.1 字符串字面量类型
字符串字面量类型用来约束取值只能是某几个字符串中的一个。
type Direction = 'North' | 'East' | 'South' | 'West';
function move(distance: number, direction: Direction) {
// ...
}
move(1, 'North'); // YES
move(1, '小芝麻'); // ERROR,类型“"小芝麻"”的参数不能赋给类型“Direction”的参数。
上例中,我们使用 type
定了一个字符串字面量类型 Direction
,它只能取四种字符串中的一种。
注意,类型别名与字符串字面量类型都是使用 type
进行定义。
字符串字面量 VS 联合类型
- 字符串字面量类型用来约束取值只能是某
几个字符串
中的一个,联合类型表示取值可以为多种类型
中的一种 - 字符串字面量 限定了使用该字面量的地方仅接受特定的值,联合类型 对于值并没有限定,仅仅限定值的类型需要保持一致
3.2 数字字面量类型
与字符串字面量类似
type Direction = 11 | 12 | 13
function move(distance: number, direction: Direction) {
// ...
}
move(1, 11); // YES
move(1, 1); // ERROR,类型“1”的参数不能赋给类型“Direction”的参数。
4、索引类型 keyof
- 使用索引类型,编译器就能够检查使用了动态属性名的代码。
如下,从对象中选取一些属性的值组成一个新数组。
let obj = {
a: 1,
b: 2,
c: 3
}
function getValues(obj: any, keys: string[]) {
return keys.map(key => obj[key])
}
console.log(getValues(obj, ['a', 'b'])) // [ 1, 2 ]
console.log(getValues(obj, ['a', 'f'])) // [ 1, undefined ]
上例中,我们获取 obj
中的 f
属性时,因为 obj
中并不存在 f
属性,所以为 undefined
, 那么我们想让当获取的值不存在的时候抛出异常在 TS 中应该怎么写呢?
主要由以下三点即可完成:
- 索引类型的查询操作符:
keyof T
(表示类型 T 的所有公共属性的字面量的联合类型)- 索引访问操作符:
T[K]
- 泛型约束:
T extends U
下面我们开始根据上述条件改造getValues
函数
- 首先我们需要一个泛型
T
它来代表传入的参数obj
的类型,因为我们在编写代码时无法确定参数obj
的类型到底是什么,所以在这种情况下要获取obj
的类型必须用面向未来的类型--泛型。
function getValues< T >(obj: T, keys: string[]) {
return keys.map(key => obj[key])
}
- 那么传入的第二个参数
keys
,它的特点就是数组的成员必须由参数obj
的属性名称构成,这个时候我们很容易想到刚学习的操作符keyof
,keyof T
代表参数obj
类型的属性名的联合类型,我们的参数keys
的成员类型K
则只需要约束到keyof T
即可。
function getValues<T, K extends keyof T>(obj: T, keys: K[]) {
return keys.map(key => obj[key])
}
- 返回值就是,我们通过类型访问符
T[K]
便可以取得对应属性值的类型,他们的数组T[K][]
正是返回值的类型。
function getValues<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map(key => obj[key])
}
此时我们的函数就彻底改造完了,接下来打印下试试:
console.log(getValues(obj, ['a', 'b'])) // [ 1, 2 ]
console.log(getValues(obj, ['a', 'f'])) // ERROR 不能将类型“"f"”分配给类型“"a" | "b" | "c"”。
我们用索引类型结合类型操作符完成了 TypeScript
版的 getValues
函数,它不仅仅有更严谨的类型约束能力,也提供了更强大的代码提示能力。
5、映射类型 in
一个常见的任务是将一个已知的类型每个属性都变为可选的:
interface Person {
name: string
age: number
gender: 'male' | 'female'
}
这个时候映射类型就派上用场了,在定义的时候用 in 操作符去批量定义类型中的属性, 映射类型的语法是
[K in Keys]
:
- K:类型变量,依次绑定到每个属性上,对应每个属性名的类型
- Keys:字符串字面量构成的联合类型,表示一组属性名(的类型)
那么我们应该如何操作呢?
-
首先,我们得找到
Keys
,即字符串字面量构成的联合类型,这就得使用上面提到的keyof
操作符,我们传入的类型是Person
,得到keyof Person
,即传入类型Person
的属性名的联合类型。 -
然后我们需要将
keyof Person
的属性名称一一映射出来[key in keyof Person]
,如果我们要把所有的属性成员变为可选类型,那么需要Person[key]
取出相应的属性值,最后我们重新生成一个可选的新类型{ [key in keyof Person]?: Person[key] }
。
用类型别名表示就是:
type PartPerson = {
[key in keyof Person]?: Person[key]
}
let p1: PartPerson = {}
// 也可以使用泛型
type Part<T> = {
[key in keyof T]?: T[key]
}
let p2: Part<Person> = {}
确实所有属性都变成了可选类型
5.1 内置工具映射类型
- TS 中内置了一些工具类型来帮助我们更好地使用类型系统(可以在 lib.es5.d.ts 中查看实现原理)
interface obj {
a: string;
b: number;
c: boolean;
}
5.1.1 Partial
- Partial<T> 可以将传入的属性由非可选变为可选返回:
type PartialObj = Partial<obj>
5.1.2 Required
- Required<T> 可以将传入的属性变为必选返回:
type RequiredObj = Required<PartialObj>
5.1.3 Readonly
- Readonly<T> 可以将传入的属性变为只读返回:
type ReadonlyObj = Readonly<obj>
5.1.4 Pick
- Pick<T, K extends keyof T> 能够帮助我们从传入的属性中摘取某项返回
type PickObj = Pick<obj, 'a' | 'b'>
// 从 obj 中 摘取 a 和 b 属性返回
interface Animal {
name: string
age: number
}
// 摘取 Animal 中的 name 属性
type AnimalSub = Pick<Animal, 'name'> // {name: string}
let a: AnimalSub = { name: '金色小芝麻' }
5.1.5 Record
- Record<T, U> 将字符串字面量类型
T
中所有字串变量作为新类型的Key
值,U
类型作为Key
的类型
type RecordObj = Record<'x' | 'y', obj>
5.2 映射类型修饰符的控制
- TypeScript 2.8 中增加了对映射类型修饰符的控制
- 具体而言,一个
readonly
或?
修饰符在一个映射类型里可以用前缀+
或-
来表示这个修饰符应该被添加或删除 - TS 中部分内置工具类型就利用了这个特性(Partial、Required、Readonly...), 这里我们可以参考 Partial、Required 的实现
type MutableRequired<T> = { -readonly [P in keyof T]-?: T[P] }; // 移除readonly和?
type ReadonlyPartial<T> = { +readonly [P in keyof T]+?: T[P] }; // 添加readonly和?
不带+
或-
前缀的修饰符与带+
前缀的修饰符具有相同的作用。因此上面的ReadonlyPartial<T>
类型与下面的一致
type ReadonlyPartial<T> = { readonly [P in keyof T]?: T[P] }; // 添加readonly和?
6、条件类型
TypeScript 2.8引入了条件类型,它能够表示非统一的类型。 条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:
T extends U ? X : Y
上面的类型意思是,若T
能够赋值给U
,那么类型是X
,否则为Y
。有点类似于JavaScript
中的三元条件运算符。
6.1 分布式条件类型
当条件类型中被检查的类型是无类型参数(naked type parameter)时,它会被称为分布式条件类型(Distributive Conditional Type)。其特殊之处在于它能自动分布联合类型,举个简单的例子,假设T的类型是A | B | C,那么它会被解析成三个条件分支,如下所示。
A|B|C extends U ? X : Y
// 等价于
A extends U ? X : Y | B extends U ? X : Y | C extends U ? X : Y
无类型参数(naked type parameter)
如果T或U包含类型变量,那么就得延迟解析,即等到类型变量都有具体类型后才能计算出条件类型的结果。在下面的示例中,创建了一个Person接口,声明的全局函数add()的返回值类型会根据是否是Person的子类型而改变,并且在泛型函数func()中调用了add()函数。
interface Person { name: string; age: number; getName(): string; } declare function add\<T>(x: T): T extends Person ? string : number; function func\<U>(x: U) { let a = add(x); let b: string | number = a; }
虽然a变量的类型尚不确定,但是条件类型的结果不是string就是number,因此可以成功的赋给b变量。
- 分布式条件类型可以用来过滤联合类型,如下所示,Filter<T, U>类型可从T中移除U的子类型。
type Filter<T, U> = T extends U ? never : T;
type T1 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T2 = Filter<string | number | (() => void), Function>; // string | number
- 分布式条件类型也可与映射类型配合使用,进行针对性的类型映射,即不同源类型对应不同映射规则,例如映射接口的方法名,如下所示。
interface Person {
name: string;
age: number;
getName(): string;
}
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
type T3 = FunctionPropertyNames<Person>; // "getName"
6.2 类型推断 infer
在条件类型的extends
子句中,允许通过infer
声明引入一个待推断的类型变量,并且可出现多个同类型的infer
声明,例如用infer
声明来提取函数的返回值类型,如下所示。有一点要注意,只能在true
分支中使用infer
声明的类型变量。
type Func<T> = T extends (...args: any[]) => infer R ? R : any;
- 当函数具有重载时,就取最后一个函数签名进行推断,如下所示,其中ReturnType<T>是内置的条件类型,可获取函数类型T的返回值类型。
declare function load(x: string): number;
declare function load(x: number): string;
declare function load(x: string | number): string | number;
type T4 = ReturnType<typeof load>; // string | number
- 注意,无法在正常类型参数的约束子语句中使用infer声明,如下所示。
type Func<T extends (...args: any[]) => infer R> = R; // 错误,不支持
- 但是可以将约束里的类型变量移除,并将其转移到条件类型中,就能达到相同的效果,如下所示。
type AnyFunction = (...args: any[]) => any;
type Func<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;
6.3 预定义条件类型
TypeScript 2.8在lib.d.ts
里增加了一些预定义的条件类型(可以在 lib.es5.d.ts 中查看;):
- 1)Exclude<T, U>:从T中移除掉U的子类型。
- 2)Extract<T, U>:从T中筛选出U的子类型。
- 3)NonNullable<T>:从T中移除null与undefined。
- 4)ReturnType<T>:获取函数返回值类型。
- 5)InstanceType<T>:获取构造函数的实例类型。
6.3.1 Exclude
- 从 T 可分配给的类型中排除 U
type E = Exclude<string | number, string>
let e: E = 10 // number
6.3.2 Extract
- 从 T 可分配给的类型中提取 U
type E = Extract<string | number, string>
let e: E = '1' // string
6.3.3 NonNullable
- 从 T 中排除 null 和 undefined
type E = NonNullable<string | number | null | undefined>
let e: E = '1' // string | number
6.3.4 ReturnType
- 获取函数类型的返回类型
function getUserInfo() {
return { name: '金色小芝麻', age: 10 }
}
type UserInfo = ReturnType<typeof getUserInfo>
let user: UserInfo = { name: '金色小芝麻', age: 10 } // {name: string;age: number;}
6.3.5 InstanceType
- 获取构造函数的实例类型
class Person {
name: string
constructor(name: string) {
this.name = name
}
}
type P = InstanceType<typeof Person>
let p: P = new Person('1') // Person
参考文献
[1]. TypeScript中文网
转载自:https://juejin.cn/post/7008872026978910221