简述
前面我们讨论了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)
letfoo:'Hello'foo='Hello'// ok
foo='Bar';// Error: 'Bar' 不能赋值给类型 'Hello'
typeCF=typeoffoo// type CF = "Hello"
constfoo1='World'typeCF1=typeoffoo1// type CF1 = "World"
- 字面量联合类型 单纯的字面量类型并不是很实用,更多的场景是联合类型的形式出现,用于有限的、有特定关联的固定值类型。例如四季名称、星期、方向名称、色子的六个面等等。类型'Hello'不同于其实例值'Hello',也不同于string类型的实例‘Hello’
typeCardinalDirection='North'|'East'|'South'|'West';functionmove(distance:number,direction:CardinalDirection){// ...
}move(1,'North');// ok
move(1,'Nurth');// Error
- 字面量类型与keyof 前面讲过,结合keyof可以获取类型属性键值的字面量类型的联合类型
interfacePerson{name:string;age:number;location:string;}typeK1=keyofPerson;// "name" | "age" | "location"
- 推断类型陷阱。 虽然这里使用了const声明变量,但是变量test的类型是{someProp:string},所以属性someProp被推断为string类型。string类型的实例'foo'无法分配给字面量类型'foo'
functioniTakeFoo(foo:'foo'){}consttest={someProp:'foo'};iTakeFoo(test.someProp)// 类型“string”的参数不能赋给类型“"foo"”的参数
可以使用类型断言解决这个问题。下面声明常量test的时,someProp属性使用了类型断言。因此test的声明类型被推断为{someProp:'foo'},此处的'foo'是字面量类型,满足函数iTakeFoo的参数类型要求
functioniTakeFoo(foo:'foo'){}consttest={someProp:'foo'as'foo'};iTakeFoo(test.someProp);// ok
数字字面量类型(Number Literal Types)
数字字面量类型,大致跟上面的字符串字面量类型相同,可以把具体固定的值当类型使用。同时,也要注意推断类型陷阱的问题。
letzeroOrOne:0|1;zeroOrOne=0;// OK
zeroOrOne=1;// OK
zeroOrOne=2;// Error: Type '2' is not assignable to type '0 | 1'
functiongetAge(age:28){}constperson={age:28}getAge(person.age)// 类型“number”的参数不能赋给类型“28”的参数
typeTN=typeofzeroOrOne// TN = 0 | 1
- 应用实例 下面这个例子应用场景很常见,这是一个处理端口号的函数,返回值是数字字面量类型组成的联合类型。
functiongetPort(scheme:"http"|"https"):80|443{switch(scheme){case"http":return80;case"https":return443;}}consthttpPort=getPort("http");// Type 80 | 443
- 函数重载的影响 但是结合Typescript的函数重载一起使用,返回值就明确多了。
functiongetPort(scheme:"http"):80;functiongetPort(scheme:"https"):443;functiongetPort(scheme:"http"|"https"):80|443{switch(scheme){case"http":return80;case"https":return443;}}consthttpPort=getPort("http");// Type 80
consthttpsPort=getPort("https");// Type 443
枚举字面量类型(Enum Literal Types)
枚举类型同样也可以用作字面量类型。继续上面的例子,我们先声明一个包含两个端口号的枚举常量
constenumHttpPort{Http=80,Https=443}
同样利用函数重载,不过这次我们创建一个getScheme函数
functiongetScheme(port:HttpPort.Http):"http";functiongetScheme(port:HttpPort.Https):"https";functiongetScheme(port:HttpPort):"http"|"https"{switch(port){caseHttpPort.Http:return"http";caseHttpPort.Https:return"https";}}constscheme=getScheme(HttpPort.Http);// Type "http"
- 枚举常量类型编译规则 枚举常量没有运行时表现形式(除非你设置了preserveConstEnums编译选项),编译器将会直接编译成相应的值,而不是变量。看下面编译结果,和s编译成了80和443
这也是提高代码性能的小技巧
functiongetScheme(port){switch(port){case80:return"http";case443:return"https";}}varscheme=getScheme(80);
二、never
- never类型定义 never是Typescript类型中的底部类型。底部类型是没有值的类型,也称为零类型或者空类型。底部类型是所有类型的子类,用符号表示是(⊥)。
- never应用场景 通常下面两种情况会用到never类型:
- 用于表示不会有返回值的函数的返回类型:例如,永远循环的函数,始终抛出异常信号的函数等
- 类型变量受永不可能为真的条件限制,由于类型保护机制,变量类型收窄为never类型
- 不会有返回值的函数
这是一无限循环函数,没有任何的终止循环语句,函数执行永远不会结束,因此不会有任何的返回值。
constsing=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");}}
下面是一个始终抛出异常的函数
constfailwith=(message:string)=>{thrownewError(message);}
在一个永远无法为真的逻辑判断中,类型会收窄为never
functioncontrolFlowAnalysisWithNever(value:string|number){if(typeofvalue==="string"){value;// Type string
}elseif(typeofvalue==="number"){value;// Type number
}else{value;// Type never
}}
- never类型的特点
- never是任意类型的子类型,并且可以分配给任意类型
letn:neverleta:string=nletb:number=nletc:boolean=nletd:'d'=nlete:never=nletf:()=>void=nletg:unknown=nleth:any=nleti:symbol=n
- never没有子类型,并且除了never本身,没有类型可以分配给never类型
letn:neverletn1:never=n// ok
n='s'// 不能将类型“string”分配给类型“never”
n=1// 不能将类型“number”分配给类型“never”
n=false// 不能将类型“boolean”分配给类型“never”
n=(...arg:any)=>void;// 不能将类型“(...arg: any) => any”分配给类型“never”
n=newArray()// 不能将类型“any[]”分配给类型“never”
n=Symbol()// 不能将类型“symbol”分配给类型“never”
n={}// 不能将类型“{}”分配给类型“never”
- 在一个没有返回值标注的函数表达式或箭头函数中, 如果函数没有 return 语句, 或者仅有表达式类型为 never 的 return 语句, 并且函数的终止点无法被执行到 (按照控制流分析), 则推导出的函数返回值类型是 never
// Return type: never
constfailwith1=function(message:string){thrownewError(message);}// Return type: never
constfailwith2=(message:string)=>{thrownewError(message);}
- 这种规则并不适用于函数声明,这么做的原因为了向后兼容。因此,最合理的方式是明确的声明返回类型
// Return type: void
functionfailwith3(message:string){thrownewError(message);}
- 在一个明确指定了 never 返回值类型的函数中, 所有 return 语句 (如果有) 表达式的值必须为 never 类型, 且函数不应能执行到终止点
// 报错, 返回“never”的函数不能具有可访问的终结点
functiontypeWithNever(arg:string|number):never{letrst:neverletrst1:stringletrst2:numberif(typeofarg==='string'){returnrst1// 报错,不能将类型“string”分配给类型“never”
}elseif(typeofarg==='number'){returnrst2// 报错, 不能将类型“number”分配给类型“never”
}}
使函数无法执行到终止点的方式有很多种,比如,创建永远都无法执行的条件分支;增加无限循环语句;抛出异常等 另外,每个分支返回类型必须是never,包括永远无法执行的分支
functiontypeWithNever(arg:string|number):never{letrst:neverif(typeofarg==='string'){returnrst}elseif(typeofarg==='number'){returnrst}else{returnrst}}// or
functiontypeWithNever(arg:string|number):never{letrst:neverletrst1:stringletrst2:numberif(typeofarg==='string'){returnrst}elseif(typeofarg==='number'){returnrst}while(true){}}// or
functiontypeWithNever(arg:string|number):never{letrst:neverletrst1:stringletrst2:numberif(typeofarg==='string'){returnrst}elseif(typeofarg==='number'){returnrst}throw('get never')}
never类型是底部类型,是任意类型的子类型,所以任意类型和never的交叉类型都是never。
typeT1=number&never;// never
typeT2=string&never;// never
typeT3='a'&never;// never
typeT4=1&never;// never
typeT5=true&never;// never
typeT6=any&never;// never
typeT7=unknown&never;// never
typeT8=never&never;// never
可以分配给never的只有never,而且never可以分配给其他任何类型。所以联合never是没有意义的。就行+0跟没有添结果是一样的
typeT1=number|never;// number
typeT2=string|never;// string
typeT3='a'|never;// 'a'
typeT4=1|never;// 1
typeT5=true|never;// true
typeT6=any|never;// any
typeT7=unknown|never;// unknown
typeT8=never|never;// never
利用这个特性可以做很多事情,比如筛选。先把利用条件类型需要过滤的类型转换成never,然后利用联合类型过滤掉never
typeFilter<T,U>=TextendsU?T:nevertypeR=Filter<'a'|2|false|'b',string>// R = 'a'|'b'
按照分布式条件类型机制,Filter<'a' | 2 | false | 'b', string>将被分解成如下代码
typeR=|('a'extemdsstring?'a':never)|(2extendsstring?2:never)|(falseextendsstring?false:never)|('b'extendsstring?'b':never)
进一步将被解析,最终得到 'a' | 'b'
typeR='a'|never|never|'b'typeR='a'|'b'
三、unknown类型
- 定义 unknown是所有类型的父类型,任何类型都是unknown的子类型。跟never类型相对的,unknown是顶部类型(Top Type),符号是(⊤)
- 特点
- 任何类型的实例都可以分配给unknown
leta:unknowna=Symbol('deep dark fantasy')a={}a=falsea='114514'a=1919n
- unknown只能分配给unknown,不能分配给其他任何类型
leta:unknownletb:string=a// 不能将类型“unknown”分配给类型“string”
letc:number=a// 不能将类型“unknown”分配给类型“number”
letd:boolean=a// 不能将类型“unknown”分配给类型“boolean”
lete:()=>void=a// 不能将类型“unknown”分配给类型“() => void”
letf:symbol=a// 不能将类型“unknown”分配给类型“symbol”
letg:unknown=a// ok
- 任意类型和unknown的交叉类型都是其本身
typeT=string&unknown// string
typeT1=number&unknown// number
typeT2=boolean&unknown// boolean
typeT3=symbol&unknown// symbol
typeT4=never&unknown// never
typeT5=any&unknown// any
typeT6='a'&unknown// 'a'
typeT7=1&unknown// 1
typeT8=false&unknown// false
typeT9=string[]&unknown// string[]
- 任意类型和unknown的联合类型都是unknown
typeT=string|unknown// unknown
typeT1=number|unknown// unknown
typeT2=boolean|unknown// unknown
typeT3=symbol|unknown// unknown
typeT4=never|unknown// unknown
typeT5=any|unknown// unknown
typeT6='a'|unknown// unknown
typeT7=1|unknown// unknown
typeT8=false|unknown// unknown
typeT9=string[]|unknown// unknown
四、交叉类型(Intersection Types)
- 定义 交叉类型用&操作符把几个类型的成员合并,形成一个拥有这几个类型所有成员的新类型。
- 声明交叉类型
需要注意的是,不能从字面上理解交叉类型,它不是几个类型的交集,而是具备所有类型成员的新类型。可以把操作符&理解成 and,A & B 表示同时包含 A 和 B 的所有成员,我们可以直接使用它,而不需要判断是否存在该属性
interfaceIPerson{name:string;age:number;}interfaceIStudent{grade:number;}typeIIT=IPerson&IStudent/** IIT = {
name: string;
age: number;
grade: number;
}*/letuser:IIT={name:'Joi',age:12,grade:6}
如果交叉类型的有相同的成员名称,原则上会继续交叉合并。下面通过实例验证一下
interfaceA{name:string;age:number;child:{name:string;age:number;}}interfaceB{name:string;age:number;child:{male:boolean}}typeC=A&Bletc:C={name:'zhangsan',age:28,child:{name:'lisi',age:6}}
上面例子中,故意缺省male成员,下面是vs code编辑提示的错误信息。很显然交叉类型同名成员会继续进行交叉合并,最终child是两个类型的交叉类型。
- 不存在的类型 需要注意,交叉类型的结果可能是不存在的类型,会推断为never类型。例如,基本类型string和number的交叉类型,显然是不存在既是string又是number类型的值,最终交叉类型会被推断为never类型。
举例说明,当把字符串类型实例“zhangsan”赋值给name属性,就会报“不能将类型“string”分配给类型“never”错误”
interfaceA{name:string;age:number;}interfaceB{name:number;age:number;}typeC=A&Bletc:C={name:'zhangsan',// 不能将类型“string”分配给类型“never”
age:12}
如果有兴趣,思考一下,下面这个问题。如果同名类型成员有readonly和?修饰符,又会发生什么呢?
interfaceIPerson{name:string;age:number;}interfaceIStudent{readonlyname?:string;grade:number;}typeIIT=IPerson&IStudentletuser:IIT={age:12,grade:6}user.name='lisi'
- 不同基本类型之间的交叉类型,或者不同字面量类型之间的交叉类型都是never
因为不存在同时属于两个基本类型的变量,所以不同基本类型交叉类型是不存在的类型never;不存在一个字面量属于两个不同的字面量类型,所以不同的字面量类型的交叉类型也是never
typeT1='A'&'B'typeT2=1&2typeT3=false&truetypeT4=string&booleantypeT5=string&numbertypeT6=string&symboltypeT7=number&booleantypeT8=number&symboltypeT9=boolean&symbol
联合类型(Union Types)
- 定义 联合类型(Union Types)可以通过管道(|)将变量设置多种类型。
- 声明联合类型
操作符“|”可以理解为“或”,类型A或者类型B,可以用 A | B表示。
看下面例子,我们声明了一个变量val,它的类型我们定义为string | number。这意味val可以赋值string类型的值,也可以赋值number类型的值
letval:string|numberval=12console.log("数字为 "+val)let="Hello"console.log("字符串为 "+val)
前面的讨论中,曾经频繁使用字面量联合类型。比如keyof操作符可以获取类型成员名称组成的联合类型;进而还可以用in操作符循环操作,映射成新的类型
typePerson={name:string;age:number;male:boolean;}typePsersonMap={[PinkyeofPerson]?:Person[P]]}/**
* type Person = {
name?:string;
age?:number;
male?:boolean;
}
*/
当类型中含有字面量成员时,我们可以用该类型成员来辨析联合类型
下面例子,Square 和 Rectangle有共同成员kind,因此kind存在于他们的联合类型Shape中
interfaceSquare{kind:'square';size:number;}interfaceRectangle{kind:'rectangle';width:number;height:number;}typeShape=Square|Rectangle
应用中,通过一些操作符,例如==、===、!=、!==判断kind的值,实现相应的逻辑操作。Typescript能根据操作符和相应的判断条件推断出使用的具体类型,类型会相应的收窄为Square或者Rectangle。
functionarea(s:Shape){if(s.kind==='square'){// 现在 TypeScript 知道 s 的类型是 Square
// 所以你现在能安全使用它
returns.size*s.size;}else{// 不是一个 square ?因此 TypeScript 将会推算出 s 一定是 Rectangle
returns.width*s.height;}}
- exhaustive check
这里再添加一个类型Circle,如果配置中设置了noImplicitReturns为true,为了进一步明确类型,area函数中判断条件,需要调整如下格式。但是还是会提示警告“并非所有代码路径都返回值”。
// ....
interfaceCircle{kind:'circle';radius:number;}typeShape11=Square|Rectangle|Circle// 报错, 并非所有代码路径都返回值
functionarea(s:Shape){if(s.kind==='square'){returns.size*s.size;}elseif(s.kind==='rectangle'){returns.width*s.height;}}
前面讲过,永远不可能执行的条件判断中,类型会被收窄为never。可以利用这个机制,来保证逻辑中没有未实现的条件判断,确保使用安全。下面例子,没有实现Circle的条件判断,因此else分支中类型被收窄为Circle,而不是never,所以会提示错误:不能将类型“Circle”分配给类型“never”
functionarea(s:Shape){if(s.kind==='square'){returns.size*s.size;}elseif(s.kind==='rectangle'){returns.width*s.height;}else{const_exhaustiveCheck:never=s// 报错, 不能将类型“Circle”分配给类型“never”
return_exhaustiveCheck}}
- switch
你可以通过 switch 来实现以上例子,效果跟if..else..判断是一样的
functionarea(s:Shape){switch(s.kind){case'square':returns.size*s.size;case'rectangle':returns.width*s.height;case'circle':returnMath.PI*s.radius**2;default:const_exhaustiveCheck:never=s;}}
元组(Tuple Types)
数组是一个变长的,元素类型都相同的列表。与之相对应的,元组是一个描述固定长度,不同类型元素的数组。
元组的创建格式如下:
consttuple_name:[type1,type2,type3,...typen]=[value1,value2,value3,…valuen]
一个声明并初始化的元组实例
consttp:[string,number,boolean]=['a',1,true]
- 元组长度是固定的 比如给tp[3]赋值undefined,或提示错误:长度为 "3" 的元组类型 "[string, number, boolean]" 在索引 "3" 处没有元素
- 元组索引位置上的类型顺序是固定的 比如给tp赋值调整一下顺序,会提示:不能将类型“number”分配给类型“string” 和 不能将类型“string”分配给类型“number”
consttp:[string,number,boolean]=[1,'a',true]
可以像访问数组那样,通过索引下标访问和更新元组中对应的元素
letn1=tp[0]// 'a'
letn2=tp[1]// 1
tp[0]='b'tp[1]=2letn1=tp[0]// 'b'
letn2=tp[1]// 2
如果明确声明的元组是不可修改,可以采用readonly把元组声明为只读元组
consttp2:readonly[string,number,boolean]=['a',1,true]tp2[0]='b'// 报错,无法分配到 "0" ,因为它是只读属性
类似与声明函数参数,可以通过?修饰符来定义元组的可选元素。例如,下面例子,把元素全部定义为可选,可以先初始化一个空的元组,后面再赋值。
consttp3:[string?,number?,boolean?]=[]tp3[0]='a'tp3[1]=1tp3[2]=true
元组本质是固定长度,不同元素类型的数组。所以元组具备数组所有方法和属性。如length、map、forEach、push、pop等。下面用keyof检测一下,可以直观的看到相关属性
typetpKeys=keyoftypeoftp/** type tpKeys = number | "0" | "1" | "2" | "length" | "toString" | "toLocaleString" | "pop" | "push" | "concat" | "join" | "reverse" | "shift" | "slice" | "sort" | "splice" | "unshift" | "indexOf" |......*/
解构实例
vara=[10,"axihe"]var[b,c]=aconsole.log(b)console.log(c)
rest语法实例
consttp4:[number,...string[]]=[1,'a','b','c']// 说好的固定长度呢?
typeTP=[string,number,boolean]consttp5:[number,...TP]=[1,'a',2,true]