认识Typescript常用高级类型
简述
前面我们讨论了Typescript常用类型操作符typeof、keyof、in、extends、infer等。合理的使用这些类型操作符,我们创建很多实用的类型和类型工具。总结归纳:
- typeof提供了对象转类型的方法和途径
- keyof提供了获取类型属性键值的能力
- in提供遍历操作能力
- extends提供了范围限制和条件判断能力
- infer结合extends提供了声明特定位置待推断类型的能力
总而言之,合理结合使用泛型函数、类型和操作符,使我们具备了类型编程的能力。下面我们首先详细的讨论一下常用的一些高级类型以及泛型函数,进而探讨一下类型编程技巧,最后再一起认识和解析一些常用的类型函数和工具。希望通过讨论,大家能够熟练的掌握Typescript类型和类型操作符,具备类型编程能力。
一、字面量类型
什么是字面量类型
字面量也叫直接量。计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。字面量类型是固定值表示的类型。通常我们使用的string,number,boolean等类型属于集合类型,例如string是所有字符串集合。字面量类型不同于集合类型,它只有一个类型实例,即其固定值,所以字面量类型也叫单位类型(Unit Type)\
字面量类型分类
- 字符串字面量类型(String Literal Types)
- 数字字面量类型(Number Literal Types)
- 布尔字面量类型(Boolean Literal Types)
- 枚举字面量类型(Enum Literal Types)
字符串字面量类型(String Literal Types)
- 实例声明
使用一个字符串字面量作为一个类型,例如
使用一个字符串字面量作为一个类型,例如
let foo: 'Hello'
foo = 'Hello' // ok
foo = 'Bar'; // Error: 'Bar' 不能赋值给类型 'Hello'
- 类型检测
可以使用typeof检验一下变量等类型
type CF = typeof foo // type CF = "Hello"
const foo1 = 'World'
type CF1 = typeof foo1 // type CF1 = "World"
- 字面量联合类型
单纯的字面量类型并不是很实用,更多的场景是联合类型的形式出现,用于有限的、有特定关联的固定值类型。例如四季名称、星期、方向名称、色子的六个面等等。类型'Hello'不同于其实例值'Hello',也不同于string类型的实例‘Hello’
type CardinalDirection = 'North' | 'East' | 'South' | 'West';
function move(distance: number, direction: CardinalDirection) {
// ...
}
move(1, 'North'); // ok
move(1, 'Nurth'); // Error
- 字面量类型与keyof
前面讲过,结合keyof可以获取类型属性键值的字面量类型的联合类型
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
- 推断类型陷阱。
虽然这里使用了const声明变量,但是变量test的类型是{someProp:string},所以属性someProp被推断为string类型。string类型的实例'foo'无法分配给字面量类型'foo'
function iTakeFoo(foo: 'foo') {}
const test = {
someProp: 'foo'
};
iTakeFoo(test.someProp) // 类型“string”的参数不能赋给类型“"foo"”的参数
可以使用类型断言解决这个问题。下面声明常量test的时,someProp属性使用了类型断言。因此test的声明类型被推断为{someProp:'foo'},此处的'foo'是字面量类型,满足函数iTakeFoo的参数类型要求
function iTakeFoo(foo: 'foo') {}
const test = {
someProp: 'foo' as 'foo'
};
iTakeFoo(test.someProp); // ok
数字字面量类型(Number Literal Types)
数字字面量类型,大致跟上面的字符串字面量类型相同,可以把具体固定的值当类型使用。同时,也要注意推断类型陷阱的问题。
let zeroOrOne: 0 | 1;
zeroOrOne = 0;
// OK
zeroOrOne = 1;
// OK
zeroOrOne = 2;
// Error: Type '2' is not assignable to type '0 | 1'
function getAge(age:28){
}
const person = {
age: 28
}
getAge(person.age) // 类型“number”的参数不能赋给类型“28”的参数
type TN = typeof zeroOrOne // TN = 0 | 1
- 应用实例
下面这个例子应用场景很常见,这是一个处理端口号的函数,返回值是数字字面量类型组成的联合类型。
function getPort(scheme: "http" | "https"): 80 | 443 {
switch (scheme) {
case "http":
return 80;
case "https":
return 443;
}
}
const httpPort = getPort("http"); // Type 80 | 443
- 函数重载的影响
但是结合Typescript的函数重载一起使用,返回值就明确多了。
function getPort(scheme: "http"): 80;
function getPort(scheme: "https"): 443;
function getPort(scheme: "http" | "https"): 80 | 443 {
switch (scheme) {
case "http":
return 80;
case "https":
return 443;
}
}
const httpPort = getPort("http"); // Type 80
const httpsPort = getPort("https"); // Type 443
枚举字面量类型(Enum Literal Types)
枚举类型同样也可以用作字面量类型。继续上面的例子,我们先声明一个包含两个端口号的枚举常量
const enum HttpPort {
Http = 80,
Https = 443
}
同样利用函数重载,不过这次我们创建一个getScheme函数
function getScheme(port: HttpPort.Http): "http";
function getScheme(port: HttpPort.Https): "https";
function getScheme(port: HttpPort): "http" | "https" {
switch (port) {
case HttpPort.Http:
return "http";
case HttpPort.Https:
return "https";
}
}
const scheme = getScheme(HttpPort.Http);
// Type "http"
- 枚举常量类型编译规则
枚举常量没有运行时表现形式(除非你设置了preserveConstEnums编译选项),编译器将会直接编译成相应的值,而不是变量。看下面编译结果,HttpPort.Http和HttpPort.Https编译成了80和443
这也是提高代码性能的小技巧\
function getScheme(port) {
switch (port) {
case 80:
return "http";
case 443:
return "https";
}
}
var scheme = getScheme(80);
二、never
- never类型定义
never是Typescript类型中的底部类型。底部类型是没有值的类型,也称为零类型或者空类型。底部类型是所有类型的子类,用符号表示是(⊥)。
- never应用场景
通常下面两种情况会用到never类型:
1. 用于表示不会有返回值的函数的返回类型:例如,永远循环的函数,始终抛出异常信号的函数等
2. 类型变量受永不可能为真的条件限制,由于类型保护机制,变量类型收窄为never类型
- 不会有返回值的函数
这是一无限循环函数,没有任何的终止循环语句,函数执行永远不会结束,因此不会有任何的返回值
const sing = function() {
while (true) {
console.log("Never gonna give you up");
console.log("Never gonna let you down");
console.log("Never gonna run around and desert you");
console.log("Never gonna make you cry");
console.log("Never gonna say goodbye");
console.log("Never gonna tell a lie and hurt you");
}
}
- 始终抛出异常的函数
下面是一个始终抛出异常的函数
const failwith = (message: string) => {
throw new Error(message);
}
- 永不可能为真的逻辑判断下类型收窄
在一个永远无法为真的逻辑判断中,类型会收窄为never
function controlFlowAnalysisWithNever(
value: string | number
) {
if (typeof value === "string") {
value; // Type string
} else if (typeof value === "number") {
value; // Type number
} else {
value; // Type never
}
}
- never类型的特点
因为never是底部类型,是所有其它类型的子类型,所以它具备以下特征:
1、never是任意类型的子类型,并且可以分配给任意类型
let n: never
let a: string = n
let b: number = n
let c: boolean = n
let d: 'd' = n
let e: never = n
let f: () => void = n
let g: unknown = n
let h: any = n
let i: symbol = n
2、never没有子类型,并且除了never本身,没有类型可以分配给never类型
let n: never
let n1:never = n // ok
n = 's' // 不能将类型“string”分配给类型“never”
n = 1 // 不能将类型“number”分配给类型“never”
n = false // 不能将类型“boolean”分配给类型“never”
n = (...arg: any) => void; // 不能将类型“(...arg: any) => any”分配给类型“never”
n = new Array() // 不能将类型“any[]”分配给类型“never”
n = Symbol() // 不能将类型“symbol”分配给类型“never”
n = {} // 不能将类型“{}”分配给类型“never”
3、在一个没有返回值标注的函数表达式或箭头函数中, 如果函数没有 return 语句, 或者仅有表达式类型为 never 的 return 语句, 并且函数的终止点无法被执行到 (按照控制流分析), 则推导出的函数返回值类型是 never
// Return type: never
const failwith1 = function(message: string) {
throw new Error(message);
}
// Return type: never
const failwith2 = (message: string) => {
throw new Error(message);
}
4、这种规则并不适用于函数声明,这么做的原因为了向后兼容。因此,最合理的方式是明确的声明返回类型
// Return type: void
function failwith3(message: string) {
throw new Error(message);
}
5、在一个明确指定了 never 返回值类型的函数中, 所有 return 语句 (如果有) 表达式的值必须为 never 类型, 且函数不应能执行到终止点
// 报错, 返回“never”的函数不能具有可访问的终结点
function typeWithNever(arg: string | number): never {
let rst: never
let rst1: string
let rst2: number
if (typeof arg === 'string') {
return rst1 // 报错,不能将类型“string”分配给类型“never”
} else if (typeof arg === 'number') {
return rst2 // 报错, 不能将类型“number”分配给类型“never”
}
}
使函数无法执行到终止点的方式有很多种,比如,创建永远都无法执行的条件分支;增加无限循环语句;抛出异常等 另外,每个分支返回类型必须是never,包括永远无法执行的分支
function typeWithNever(arg: string | number): never {
let rst: never
if (typeof arg === 'string') {
return rst
} else if (typeof arg === 'number') {
return rst
} else {
return rst
}
}
// or
function typeWithNever(arg: string | number): never {
let rst: never
let rst1: string
let rst2: number
if (typeof arg === 'string') {
return rst
} else if (typeof arg === 'number') {
return rst
}
while(true){
}
}
// or
function typeWithNever(arg: string | number): never {
let rst: never
let rst1: string
let rst2: number
if (typeof arg === 'string') {
return rst
} else if (typeof arg === 'number') {
return rst
}
throw('get never')
}
- 任意类型和never的交叉类型都是never
never类型是底部类型,是任意类型的子类型,所以任意类型和never的交叉类型都是never。
type T1 = number & never; // never
type T2 = string & never; // never
type T3 = 'a' & never; // never
type T4 = 1 & never; // never
type T5 = true & never; // never
type T6 = any & never; // never
type T7 = unknown & never; // never
type T8 = never & never; // never
- 任意类型和never的联合类型都是其本身
可以分配给never的只有never,而且never可以分配给其他任何类型。所以联合never是没有意义的。就行+0跟没有添结果是一样的
type T1 = number | never; // number
type T2 = string | never; // string
type T3 = 'a' | never; // 'a'
type T4 = 1 | never; // 1
type T5 = true | never; // true
type T6 = any | never; // any
type T7 = unknown | never; // unknown
type T8 = never | never; // never
- 应用场景
利用这个特性可以做很多事情,比如筛选。先把利用条件类型需要过滤的类型转换成never,然后利用联合类型过滤掉never
type Filter<T, U> = T extends U ? T : never
type R = Filter<'a' | 2 | false | 'b', string> // R = 'a'|'b'
按照分布式条件类型机制,Filter<'a' | 2 | false | 'b', string>将被分解成如下代码
type R =
| ('a' extemds string ? 'a':never)
| (2 extends string ? 2:never)
| (false extends string ? false:never)
| ('b' extends string ? 'b':never)
进一步将被解析,最终得到 'a' | 'b'
type R = 'a' | never | never | 'b'
type R = 'a' | 'b'
三、unknown类型
unknown是所有类型的父类型,任何类型都是unknown的子类型。跟never类型相对的,unknown是顶部类型(Top Type),符号是(⊤)
- unknown类型特点
因为unkonwn类型是顶部类型,是所有其他类型的父类型,所以它具备了以下特征:
1、任何类型的实例都可以分配给unknown
let a: unknown
a = Symbol('deep dark fantasy')
a = {}
a = false
a = '114514'
a = 1919n
2、unknown只能分配给unknown,不能分配给其他任何类型
let a: unknown
let b: string = a // 不能将类型“unknown”分配给类型“string”
let c: number = a // 不能将类型“unknown”分配给类型“number”
let d: boolean = a // 不能将类型“unknown”分配给类型“boolean”
let e: ()=>void = a // 不能将类型“unknown”分配给类型“() => void”
let f: symbol= a // 不能将类型“unknown”分配给类型“symbol”
let g: unknown = a // ok
3、任意类型和unknown的交叉类型都是其本身
type T = string & unknown // string
type T1 = number & unknown // number
type T2 = boolean & unknown // boolean
type T3 = symbol & unknown // symbol
type T4 = never & unknown // never
type T5 = any & unknown // any
type T6 = 'a' & unknown // 'a'
type T7 = 1 & unknown // 1
type T8 = false & unknown // false
type T9 = string[] & unknown // string[]
4、任意类型和unknown的联合类型都是unknown
type T = string | unknown // unknown
type T1 = number | unknown // unknown
type T2 = boolean | unknown // unknown
type T3 = symbol | unknown // unknown
type T4 = never | unknown // unknown
type T5 = any | unknown // unknown
type T6 = 'a' | unknown // unknown
type T7 = 1 | unknown // unknown
type T8 = false | unknown // unknown
type T9 = string[] | unknown // unknown
四、交叉类型(Intersection Types)
- 定义
交叉类型用&操作符把几个类型的成员合并,形成一个拥有这几个类型所有成员的新类型。
- 声明交叉类型
type I = A & B & C & D
需要注意的是,不能从字面上理解交叉类型,它不是几个类型的交集,而是具备所有类型成员的新类型。可以把操作符&理解成 and,A & B 表示同时包含 A 和 B 的所有成员,我们可以直接使用它,而不需要判断是否存在该属性\
- 实例
interface IPerson {
name: string;
age: number;
}
interface IStudent {
grade: number;
}
type IIT = IPerson & IStudent
/** IIT = {
name: string;
age: number;
grade: number;
}*/
let user: IIT = {
name: 'Joi',
age: 12,
grade: 6
}
- 类型成员冲突
如果交叉类型的有相同的成员名称,原则上会继续交叉合并。下面通过实例验证一下
interface A {
name: string;
age: number;
child: {
name:string;
age:number;
}
}
interface B {
name: string;
age: number;
child:{
male:boolean
}
}
type C = A & B
let c:C = {
name:'zhangsan',
age:28,
child:{
name:'lisi',
age:6
}
}
上面例子中,故意缺省male成员,下面是vs code编辑提示的错误信息。很显然交叉类型同名成员会继续进行交叉合并,最终child是两个类型的交叉类型。
- 不存在的类型
需要注意,交叉类型的结果可能是不存在的类型,会推断为never类型。例如,基本类型string和number的交叉类型,显然是不存在既是string又是number类型的值,最终交叉类型会被推断为never类型。
举例说明,当把字符串类型实例“zhangsan”赋值给name属性,就会报“不能将类型“string”分配给类型“never”错误”
interface A {
name: string;
age: number;
}
interface B {
name: number;
age: number;
}
type C = A & B
let c: C = {
name: 'zhangsan', // 不能将类型“string”分配给类型“never”
age: 12
}
如果有兴趣,思考一下,下面这个问题。如果同名类型成员有readonly和?修饰符,又会发生什么呢?
interface IPerson {
name: string;
age: number;
}
interface IStudent {
readonly name?: string;
grade: number;
}
type IIT = IPerson & IStudent
let user: IIT = {
age: 12,
grade: 6
}
user.name = 'lisi'
- 不同基本类型之间的交叉类型,或者不同字面量类型之间的交叉类型都是never
因为不存在同时属于两个基本类型的变量,所以不同基本类型交叉类型是不存在的类型never;不存在一个字面量属于两个不同的字面量类型,所以不同的字面量类型的交叉类型也是never
type T1 = 'A' & 'B'
type T2 = 1 & 2
type T3 = false & true
type T4 = string & boolean
type T5 = string & number
type T6 = string & symbol
type T7 = number & boolean
type T8 = number & symbol
type T9 = boolean & symbol
联合类型(Union Types)
- 定义
联合类型(Union Types)可以通过管道(|)将变量设置多种类型。
- 声明联合类型
type U = A | B
操作符“|”可以理解为“或”,类型A或者类型B,可以用 A | B表示。
- 实例
看下面例子,我们声明了一个变量val,它的类型我们定义为string | number。这意味val可以赋值string类型的值,也可以赋值number类型的值
let val:string|number
val = 12
console.log("数字为 "+ val)
let = "Hello"
console.log("字符串为 " + val)
- 应用场景
前面的讨论中,曾经频繁使用字面量联合类型。比如keyof操作符可以获取类型成员名称组成的联合类型;进而还可以用in操作符循环操作,映射成新的类型
type Person = {
name:string;
age:number;
male:boolean;
}
type PsersonMap = {
[P in kyeof Person]?:Person[P]]
}
/**
* type Person = {
name?:string;
age?:number;
male?:boolean;
}
*/
- 辨析联合类型
当类型中含有字面量成员时,我们可以用该类型成员来辨析联合类型
下面例子,Square 和 Rectangle有共同成员kind,因此kind存在于他们的联合类型Shape中
interface Square {
kind: 'square';
size: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Square | Rectangle
应用中,通过一些操作符,例如==、===、!=、!==判断kind的值,实现相应的逻辑操作。Typescript能根据操作符和相应的判断条件推断出使用的具体类型,类型会相应的收窄为Square或者Rectangle。
function area(s: Shape) {
if (s.kind === 'square') {
// 现在 TypeScript 知道 s 的类型是 Square
// 所以你现在能安全使用它
return s.size * s.size;
} else {
// 不是一个 square ?因此 TypeScript 将会推算出 s 一定是 Rectangle
return s.width * s.height;
}
}
exhaustive check
这里再添加一个类型Circle,如果tsconfig.json配置中设置了noImplicitReturns为true,为了进一步明确类型,area函数中判断条件,需要调整如下格式。但是还是会提示警告“并非所有代码路径都返回值”。
// ....
interface Circle {
kind: 'circle';
radius: number;
}
type Shape11 = Square | Rectangle | Circle
// 报错, 并非所有代码路径都返回值
function area(s: Shape) {
if (s.kind === 'square') {
return s.size * s.size;
} else if(s.kind === 'rectangle') {
return s.width * s.height;
}
}
前面讲过,永远不可能执行的条件判断中,类型会被收窄为never。可以利用这个机制,来保证逻辑中没有未实现的条件判断,确保使用安全。下面例子,没有实现Circle的条件判断,因此else分支中类型被收窄为Circle,而不是never,所以会提示错误:不能将类型“Circle”分配给类型“never”
function area(s: Shape) {
if (s.kind === 'square') {
return s.size * s.size;
} else if(s.kind === 'rectangle') {
return s.width * s.height;
} else{
const _exhaustiveCheck: never = s // 报错, 不能将类型“Circle”分配给类型“never”
return _exhaustiveCheck
}
}
switch
你可以通过 switch 来实现以上例子,效果跟if..else..判断是一样的
function area(s: Shape) {
switch (s.kind) {
case 'square':
return s.size * s.size;
case 'rectangle':
return s.width * s.height;
case 'circle':
return Math.PI * s.radius ** 2;
default:
const _exhaustiveCheck: never = s;
}
}
元组(Tuple Types)
数组是一个变长的,元素类型都相同的列表。与之相对应的,元组是一个描述固定长度,不同类型元素的数组。
- 声明元组
元组的创建格式:
const tuple_name:[type1, type2, type3,...typen] = [value1,value2,value3,…value n]
一个声明并初始化的元组实例
const tp: [string, number, boolean] = ['a', 1, true]
- 元组长度是固定的 比如给tp[3]赋值undefined,或提示错误:长度为 "3" 的元组类型 "[string, number, boolean]" 在索引 "3" 处没有元素
tp[3] = undefined
- 元组索引位置上的类型顺序是固定的 比如给tp赋值调整一下顺序,会提示:不能将类型“number”分配给类型“string” 和 不能将类型“string”分配给类型“number”
const tp: [string, number, boolean] = [1, 'a', true]
- 访问和更新元组
可以像访问数组那样,通过索引下标访问和更新元组中对应的元素
let n1 = tp[0] // 'a'
let n2 = tp[1] // 1
tp[0] = 'b'
tp[1] = 2
let n1 = tp[0] // 'b'
let n2 = tp[1] // 2
- readonly修饰符限制元组
如果明确声明的元组是不可修改,可以采用readonly把元组声明为只读元组
const tp2: readonly [string, number, boolean] = ['a', 1, true]
tp2[0] = 'b' // 报错,无法分配到 "0" ,因为它是只读属性
- 设置元组元素可选
类似与声明函数参数,可以通过?修饰符来定义元组的可选元素。例如,下面例子,把元素全部定义为可选,可以先初始化一个空的元组,后面再赋值。
const tp3: [string?, number?, boolean?] = []
tp3[0] = 'a'
tp3[1] = 1
tp3[2] = true
- 元组具备数组的所有特征
元组本质是固定长度,不同元素类型的数组。所以元组具备数组所有方法和属性。如length、map、forEach、push、pop等。下面用keyof检测一下,可以直观的看到相关属性
type tpKeys = keyof typeof tp
/** type tpKeys = number | "0" | "1" | "2" | "length" | "toString" | "toLocaleString" | "pop" | "push" | "concat" | "join" | "reverse" | "shift" | "slice" | "sort" | "splice" | "unshift" | "indexOf" |......*/
- 元组同样支持解构和rest语法
解构实例
var a =[10,"axihe"]
var [b,c] = a
console.log( b )
console.log( c )
rest语法实例
const tp4: [number, ...string[]] = [1, 'a', 'b', 'c']
// 说好的固定长度呢?
type TP = [string, number, boolean]
const tp5: [number, ...TP] = [1, 'a', 2, true]
关于Typescript的一些常用高级类型,先介绍到这里。后面我们将讨论类型编程函数-泛型,以及常用类型编程的技巧和工具