Typescript一流的类型系统支持强大的类型层面编程特性,Typescript的类型系统不仅具有极强的表现力,易于使用个,而且可通过简介明了的方式声明类型约束和关系,并且多数时候能够自动为我们推导。
本节首先讨论Typescript中的子类型,可赋值性,型变和类型拓展,加深你对前几章的认识。然后,深入说明Typescript基于控制流的类型检查特性,包括类型细化和全面性检查。
接下来讨论类型层面的一些高级编程特性:“键入”和映射对象类型,使用条件类型,自定义类型防护措施,以及类型断言和明确赋值断言等。
最后,介绍一些高级模式,尽量提升类型的安全性:伴生对象模式,改善元组的类型推导,模拟名义类型,以及安全扩展原型的方式。
首先讨论Typescript中的类型关系
3.1节
讲过可赋值性。我们已经了解了Typescript提供的多数类型,现在可以深挖一些细节。
子类型:给定两个类型A和B,假设B是A的子类型,那么在需要A的地方都可以放心使用B
超类型正好和子类型相反
多数时候,很容易判断A类型是不是B类型的子类型。例如:对number,string等类型来说,(number包含在并集类型number|string中,那么number必定是他的子类型)
但是对**参数化类型(泛型)**和其他较为复杂的类型来说,情况不那么明晰。
如果一个类型中包含其他类型(即带有类型参数的类型,如Array;带有字段的结构,如{a:number};或者函数,如(a:A)=>B),使用上述规则很难判断谁是子类型。
为了便于理解,本书作者引入了一套句法,以便使用简介且准确的语言讨论类型。这套句法不是有效的Typescript代码,只是为了使用一套语言讨论类型。
type ExistingUser = {
id:number
name:string
}
type NewUser = {
name:string
}
// 假设我们写了一个这样的代码
function deleteUser(user:{id?:number,name:string}){
delete user.id
}
let existingUser:ExistingUser = {
id:12345,
name:"red润"
}
let newUser:NewUser = {
name:"redrun"
}
deleteUser(existingUser)
existingUser.id //类型(property) id: number (值为undefined)
deleteUser接受一个对象,类型为{id?:number,name:string},我们传入的existingUser对象是{id:number,name:string}类型,注意,id属性的类型(number)是预期类型(number|undefined)的子类型。因此,{id:number,name:string}作为一个整体是{id?:number,name:string}的子类型,所以Typescript不会报错。
这里有个安全问题:把ExistingUser类型的值传给deleteUser函数之后,Typescript不知道用户的id已经被删除,所以调用deleteUser(existingUser)把该属性删除之后再读取existingUser.id,Typescript认为existingUser.id是number类型。
显然,在预期某个类型的超类型的地方使用该类型(子类型)是不安全的。由于破坏性更新(例如删除一个属性)在实际中很少见,所以Typescript放宽了要求,允许我们在预期某类型的超类型的地方使用那个类型。
那么反过来,能不能子啊预期某类型的子类型的地方使用那个类型呢?
添加一个旧用户,然后删除
type ExistingUser = {
id:number
name:string
}
type NewUser = {
name:string
}
type LegacyUser = {
id?:number|string
name:string
}
// 假设我们写了一个这样的代码
function deleteUser(user:{id?:number,name:string}){
delete user.id
}
let existingUser:ExistingUser = {
id:12345,
name:"red润"
}
let newUser:NewUser = {
name:"redrun"
}
let legacyUser:LegacyUser = {
id:4567,
name:"oldrun"
}
deleteUser(existingUser)
existingUser.id //(property) id: number
deleteUser(legacyUser)// 报错
我们传入的结构中有一个属性的类型是预期类型的超类型,Typescript报错了,这是因为id的类型是string|number|undefined,而deleteUser函数只处理了number|undefined的情况。
Typescript的行为是这样的:对预期的结构,还可以使用属性的类型<:预期类型的结构,但是不能传入属性的类型是预期类型的超类型的结构。
在类型上,我们说Typescript对结构(对象和类)的属性类型进行了
协变(covariant)
。
也就是说,如果想保证A对象可赋值给B对象,那么A对象的每个成员都必须<:B对象的对应属性。
其实,协变只是型变的四种方式之一:
在Typescript中,每个复杂类型的成员都会进行协变,包括对象,类,数组,和函数的返回类型。不过有个例外:函数的参数类型进行逆变
设计Typescript时,设计人员在易用性和安全行做出了权衡,不允许型变对象的属性类型。例如
// Obj的类型undefined|string|number interface Obj { name?:string|number } //实现Obj只能设置一个类型,不能多不能少!!!(这就是对象的属性类型不允许协变) let obj:Obj = { name:undefined }
但是会导致类型系统用起来很繁琐,而且会禁止实际安全的操作(假如deleteUser没有删除id,完全可以传入预期类型的超类型,比如我们给id传入一个字符串类型,但是Typescript不支持)
如果函数A的参数数量小于或等于函数B的参数数量,而且满足下面条件,那么函数A是函数B的子类型:
class Animal{}
class Bird extends Animal{
chirp(){}
}
class Crow extends Bird {
caw(){}
}
function chirp(bird:Bird):Bird{
bird.chirp()
return bird
}
chirp(new Animal)// 报错 函数参数类型逆变
chirp(new Bird)
chirp(new Crow)
函数返回类型的协变指一个函数是另一个函数的子类型,即一个函数的返回类<:另一个函数的返回类型。
那么参数的类型呢
function clone(f:(b:Bird)=>Bird):void{
let parent = new Bird
let babyBird = f(parent)
babyBird.chirp()
}
function animaToBird(a:Animal):Bird{
//
return new Bird
}
function crowTBird(c:Crow):Bird{
return new Bird
}
clone(animaToBird)
clone(crowTBird)//报错
为了保证一个函数可赋值给另一个函数,该函数的参数类型(包括this)都要>:另一个函数相应参数的类型。
函数不对参数和this的类型做型变。一个函数是另一个函数的子类型,必须保证该函数的参数和this的类型>:另一个函数相应参数的类型。
我们不用记诵这些规则,代码编辑器能够很好的提示。
子类型和超类型关系是静态类型语言的核心概念,对理解可赋值性也十分重要(可赋值性指在判断需要B类型的地方可否使用A类型时才用的规则)。
类型扩宽(type widening)是理解Typescript类型推导机制的关键。一般来说,Typescript在推导类型时会方宽要求,故意推导一个更宽泛的类型,而不限定为某个具体的类型。
声明变量时如果允许以后修改变量的值(let,var),变量的类型将拓宽,从字面值放大到包含该字面量的基类型
let a = "x" // string
var c = ture // boolean
// 不可变不一样
const a = 'x' // 'x'
const b = 3 // 3
我们可以显示注解类型,防止类型被拓宽:
let a:'x' = 'x' // 'x'
// b重新为a拓宽
const a = 'x'
let b = a // string
// 不让重新扩展
const c:'x' = 'x'
let d = c // 'x'
初始化null或undefined的变量扩展为any:
let a = null // any
a = 3 // any
const类型可以防止类型拓宽。这个类型用作类型断言(6.6.1节):
let a = {x:3}// {x:number}
let c = {x:3} as const // {readonly x:3}
const不仅能组织拓宽类型,还将递归把成员设为readonly,不管数据结构的嵌套层级有多深:
let d = [1,{x:2}] // (number|{x:number})
let e = [1,{x:2}] as const // readonly [1,readonly x:2]
如果想让Typescript推导的类型尽量窄一些,请使用 as const
Typescript检查一个对象是否可赋值给另一个对象类型时,也涉及到类型拓宽。
type Options = {
baseUrl:string
cacheSize?:number
tier?:'prod'|'dev'
}
class API {
constructor(private options:Options){}
}
new API({
baseURL:"xxxx",
tier:"prod"
})
// 如果有一个参数拼错了
new API({
baseURL:"xxx"
tierr:"prod"
})// 直接报错
这是编写JavaScript代码经常遇到的问题,Typescript能够捕获这种问题。可是,对象类型的成员会做协变,Typescript是如何捕获这种问题的呢?
过程是这样
Typescript之所以能够捕获这样的问题,是因为他会做多余属性检查,具体过程是:尝试把一个新鲜对象字面量类型(fresh object literal type)T赋值给另一个类型U时,如果T有不在U中的属性,Typescript将报错。
新鲜对象字面量类型指的是Typescript从对象字面量中推导出来的类型。
如果对象字面量有类型断言(6.6.1节),或者把对象字面量赋值给变量,那么新鲜字面量类型将扩宽为常规的对象类型,也就不能称其为新鲜对象字面量类型。
Typescript才用的基于流的类型推导,这是一种符号执行,类型检查器子啊检查代码过程中利用流程语句(if,?,||和switch)和类型查询(如typeof,instanceof和in)细化类型。这是一个极其便利的特性,但是很少有语言支持。
type Unit = 'cm'|'px'|'%'
let units:Unit[] = ['cm','px','%']
// 检查各个单位,如果没有匹配返回null
function parseUnit(value:string):Unit|null{
for(let i=0;i
我们可以使用parseUnit函数解析用户传入的宽度值。width的值可能是一个数字(此时假定单位为像素),可能是一个带单位的字符串,也可能是null或undefined。
下述代码对象类型做了多次细化
type Unit = 'cm'|'px'|'%'
let units:Unit[] = ['cm','px','%']
// 检查各个单位,如果没有匹配返回null
function parseUnit(value:string):Unit|null{
for(let i=0;i
JavaScript有七个假值,null,undefined,NaN,0,-0,””和false。其他均为真值
Typescript能跟随你的脚步细化类型
type UserTextEvent = {value:string}
type UserMouseEvent = {value:[number,number]}
type UserEvent = UserTextEvent|UserMouseEvent
function handle(event:UserEvent){
if(typeof event.value === 'string'){
event.value // string
// do something
return
}
event.value // [number,number]
}
Typescript知道,在if中event.value肯定是一个字符串(因为使用typeof做了检查)。这意味着,在if块后面,event.value肯定是[number,number]的元组类型,因为if后有return。
如果事情变得复杂起来
type UserTextEvent = {value:string,target:HTMLInputElement}
type UserMouseEvent = {value:[number,number],target:HTMLElement}
type UserEvent = UserTextEvent|UserMouseEvent
function handle(event:UserEvent){
if(typeof event.value === 'string'){
event.value // string
event.target // HTMLInputElement | HTMLElement!!!
// do something
return
}
event.value // [number,number]
event.target // HTMLInputElement | HTMLElement!!!
}
event.value的类型可以细化,但是event.value却不可以。handle函数的参数是UserEvent类型,可能传入UserTextEvent|UserMouseEvent类型的值。由于并集类型的成员有可能重复,因此Typescript需要一种更加可靠的方式,明确并集类型的具体情况。
为此,要使用一个字面量类型标记并集类型的各种情况。一个好的标记要满足一下情况:
更新代码
type UserTextEvent = {type:'TextEvent',value:string,target:HTMLInputElement}
type UserMouseEvent = {type:'MouseEvent',value:[number,number],target:HTMLElement}
type UserEvent = UserTextEvent|UserMouseEvent
function handle(event:UserEvent){
if(event.type === 'TextEvent'){
event.value // string
event.target // HTMLInputElement
// do something
return
}
event.value // [number,number]
event.target // HTMLElement!!!
}
现在,根据标记字段(event.type)的值细化event,Typescript知道,在if分支中event必为UserTextEvent类型。由于标记是独一无二的,所以Typescript知道二者时互斥的。
如果函数要处理并集类型的不同情况,应该使用标记。例如:在处理Flux动作,Redux规约器或React的useReducer时,其作用十分巨大。
全面性检查(也称穷尽性检查)是类型检查器所做的一项检查,为的是确保所有情况被覆盖了。这个概念源自模式匹配的语言,例如Haskell,OCaml等。
tsconfig.json noImplictReturns将提示没有return
Typescript在很多情况下,都会做全面性检查,如果发现缺少某种情况会提醒你,例如
type Weekday = 'Mon'|'Tue'|'Wed'|'Thu'|'Fri'
type Day = Weekday | 'Sat' | 'Sun'
function getNextDay(w:Weekday):Day{// 报错
switch(w){
case "Mon": return "Tue"
}
}
很明显,这里遗漏了好多天。Typescript能捕获这个错误。
错误提示我们可能遗留了某些情况,应该在最后加上一个兜底return语句,返回‘Sat’等值,也可能预示我们要调整函数的返回类型,改为Day|undefined。为每一天都加上case语句之后,这个错误将消失。由于我们注解了函数的返回类型,而没有分支能保证返回该类型的值,所以Typescript发出提醒。
function isBig(n:number){// Not all code paths return a value.
if(n>=100){
return true
}
}
对象是JavaScript的核心,为了以安全的方式描述和处理对象,Typescript提供了一系列方式。
还记得“并集类型和交集类型”一节介绍的|和&类型运算符。Typescript不只这两个。下面再介绍几个处理对象结构的类型运算符。
假设有个复杂的嵌套类型,描述从社交媒体API中得到的GraphQL API相应
type APIResponse = {
user:{
userId:string
friendList:{
count:number
friends:{
firstName:string
lastName:string
}[]
}
}
}
function getAPIResponse():Promise{
return Promise.resolve({
user:{
userId:"",
friendList:{
count:12,
friends:[{firstName:"red",lastName:"run"}]
}
}
})
}
type FriendList = APIResponse['user']['friendList']
function renderFriendList(friendList:FriendList){
}
任何结构(对象,类构造方法或类的实例)和数组都可以“键入”,例如。单个好友的类型可以这样声明:
type Friend = FriendList['friends'][number]
number是“键入”数组类型的方式,若是元组,使用0,1或其他数字字面量类型表示想“键入”的索引。
“键入”的句法与JavaScript在对象中查找字段的句法类似,这是故意为之。既然能在对象中查找值,那么也能在结构中查找类型。但是要注意,通过“键入”查找属性的类时,只能使用括号表示法,不能使用点号表示法
keyof运算符获取对象所有键的类型,合并为一个字符串字面量类型。
type ResponseKeys = keyof APIResponse // user{}
type UserKyes = keyof APIResponse['user'] //"friendList" | "userId"
type FriendListKeys = keyof APIResponse['user']['friendList'] // "friends" | "count"
把“键入”和keyof运算符结合起来,可以实现对类型安全的读值函数,读取对象中指定键的值:
function get/ 1.
O extends object,
K extends keyof O // 2.
>(o: O, k: K): O[K] {// 3.
return o[k]
}
console.log(get({ name: "red润",age:17,ok:true }, "name"));// red润
这两个类型运算符强大在于,可以准确安全的描述结构类型。。
type ActiveityLog = {
lastEvent: Date
events: {
id: string
timestamp: Date
type: 'Read' | 'Write'
}[]
}
let activeityLog: ActiveityLog = {
lastEvent: new Date(),
events: [
{ id: "123", timestamp: new Date(), type: 'Read' },
{ id: "456", timestamp: new Date(), type: 'Write' }
]
}
type Get = {
(o: O, k1: K1): O[K1]
(o: O, k1: K1, k2: K2): O[K1][K2]
(o: O, k1: K1, k2: K2, k3: K3): O[K1][K2][K3]
}
let get: Get = (object: any, ...keys: string[]) => {
let result = object
keys.forEach(k => result = result[k])
return result
}
console.log(get(activeityLog, 'events'));
console.log(get(activeityLog, 'events', 0));
console.log(get(activeityLog, 'events', 0, 'id'));
// [
// { id: '123', timestamp: 2023-07 - 30T11: 55: 54.349Z, type: 'Read' },
// { id: '456', timestamp: 2023-07 - 30T11: 55: 54.349Z, type: 'Write' }
// ]
// { id: '123', timestamp: 2023-07 - 30T11: 55: 54.349Z, type: 'Read' }
// 123
Typescript内置的Record类型用于描述有映射关系的对象。
type Obj = {
name:string,
age:number,
gender:boolean
}
let a:keyof Obj = 'name'
console.log(a);
let b:Record = {
name:'ddd',
age:15,
gender:"false"
}
console.log(b);// { name: 'ddd', age: 15, gender: 'false' }
let c:keyof typeof b = "name"
console.log(c);// name
与常规的对象索引签名相比,Record提供了更多的便利:使用常规的索引签名可以约束对象中值的类型,不过键只能用string,number,或symbol类型;使用Record还可以约束对象的键为string和number的子类型。
Typescript还提供了一种更强大的方式,即映射类型(mapped type),使用这种方式声明更安全。
type Weekday = 'Mon'|'Tue'|'Wed'|'Thu'|'Fri'
type Day = Weekday | 'Sat' | 'Sun'
let nextDay:{[K in Weekday]:Day} = {
Mon:"Tue",
Tue:"Wed",
Wed:"Thu",
Thu:"Fri",
Fri:"Sat",
}
映射类型使用独特的句法。与索引签名相同,一个对象最多有一个映射类型:
type MyMappedType = {
[key in UnionType]:ValueType
}
其实Typescript内置的Record类型也是使用映射类型实现的:
type Record = {
[P in K]:T
}
映射类型的功能比Record强大,在指定对象的键和值的类型以外,如果结合”键入“类型,还能约束特定键和值是什么类型。
下面举几个例子说明使用映射类型可以做哪些事:
type Account = {
id:number
isEmployee:boolean
notes:string[]
}
// 所有字段都是可选的
type OptionalAccount = {
[K in keyof Account]?:Account[K]// 1.
}
// 所有字段都也为null
type NullableAccount = {
[K in keyof Account]:Account[K]|null // 2.
}
// 所有字段都是只读的
type ReadonlyAccount = {
readonly [K in keyof Account]:Account[K]// 3.
}
// 所有字段都是可写的(等同于Account)
type Account2 = {
-readonly[K in keyof ReadonlyAccount]:Account[K] // 4.
}
// 所有字段都是必须得(等同于Account)
type Account3 = {
[K in keyof OptionalAccount]-?:Account[K]// 5.
}
减号(-)运算符有个对应的(+)运算符。一般不直接使用加好运算符,因为它通常蕴含在其他运算符中。在映射类型中,readonly等效于+readonly,?等效于+?、+的存在只是为了确保整体协调。
前一节讨论的映射类型非常有用,Typescript内置了一些
Record
键的类型为Keys,值的类型为Values的对象。
Partial
把Object中的每个字段都标记为可选的
Required
把Object中的每个字段都标记为必须得
Readonly
把Object中的每个字段都标记为只读的
Pick
返回Object的子类型,只含指定的Keys
伴生对象源自Scala,目的是把同名的对象和类配对在一起。在Typescript中也有类似的模式,而且作用相似,即把类型和对象配对在一起,我们也称之为伴生对象模式
伴生对象模式是下面这样的:
type Currency = {
unit: "EUR" | "GBP" | "JPY" | "USD",
value: number
}
let Currency:any = {
DEFAULT: "USD",
from(value:number,unit = Currency.DEFAULT):Currency{
return {unit,value}
}
}
console.log(Currency.DEFAULT);
console.log(Currency.from(123));
type Demo = {
name:string
test(name:string):boolean
}
let Demo = {
name:"demo",
test(name:Demo["name"]):boolean {
return false
},
}
Demo.test('a')
Typescript中的类型和值分别在不同的命名空间中。10.4节将深入说明
。这意味着,在同一个作用域中,可以有同名(这里的Currency)的类型和值。伴生对象模式在彼此独立的命名空间中两次声明相同的名称,一个是类型,另一个是值。
这种模式有几个不错的性质。首先,可以把语义上归属为同一名称的类型和值放在一起。其次,使用方可以一次性导入二者。
import {Currency} from './Currency'
let amountDue:Currency ={// 1.
unit:'JPY',
value:23344
}
let otherAmountDue = Currency.from(330,"EUR")// 2.
如果一个类型和一个对象在语义上 有关联,就可以使用伴生对象模式,由对象提供操作类型的使用方法。
本节讲述函数类型常用的几种高级技术。
Typescript在推导元组的类型时会放宽要求,推导的结果尽量宽泛,不在乎元组的长度和各位置的类型。
let a = [1,true] //(number|boolean)[]
然而,有时候我们希望推导的结果更严格一些,把上例中的a试作固定长度的元组,而不是数组。当然,我们可以使用类型断言把元组转换成元组类型(6.6.1节),也可以使用as const断言(const类型)把元组标记为只读的,尽量收窄推导出的元组类型。
我们可以利用剩余参数的类型方式,收窄推导结果,并标记为只读。
function tuple/ 1.
T extends unknown[] // 2.
>(...ts:T//3.):T{//4.
return ts//5.
}
let a = tuple(1,true) // [number, boolean]
在某些情况下,比如函数的返回值,只说函数返回boolean还不够
function isString(a:unknown):boolean{
return typeof a === 'string'
}
let b = isString('a')// true
let c = isString(false)// false
console.log(b,c);
看起来没问题,那么,在实际使用中,isString函数的效果如何
function isString(a:unknown):boolean{
return typeof a === 'string'
}
let b = isString('a')// true
let c = isString(false)// false
console.log(b,c);
function parseInput(input:string|number){
let formattedInput:string
if(isString(input)){
formattedInput = input.toUpperCase()// 报错
}
}
细化类型时可以使用的typeof运算符(6.1.5节),在这里怎么不起作用了。
类型细化的能力是有限的,只能细化当前作用域中变量的类型,一旦离开这个作用域,类型细化能力不会随之转移到新作用域中。
isString函数返回一个布尔值,但是我们要让类型检查器知道,当返回的布尔值是true时,表明我们传给isString函数的是一个字符串。为此,我们要使用用户定义的类型防护措施:
function isString(a:unknown):a is string{
return typeof a === 'string'
}
let b = isString('a')// true
let c = isString(false)// false
console.log(b,c);
function parseInput(input:string|number){
let formattedInput:string
if(isString(input)){
formattedInput = input.toUpperCase()
console.log(formattedInput);
}
}
parseInput("red润")
类型防护措施是Typescript内置的特性,是typeof和instancecof细化类型的背后机制。可是有时我们需要自定义类型防护措施的能力,is运算符就起这个作用。如果函数细化了参数的类型,而且返回一个布尔值,我们可以使用用户定义的类型防护措施确保类型的细化能在作用域之间转移,在使用该函数的任何地方都起作用。
用户定义的类型防护措施只限于一个参数,但是不限于简单的类型:
type LegacyDialog = //
type Dialog = //
function isLegacyDialog(dialog:LegacyDialog|Dialog):dialog is LegacyDialog{
//}
用户定义的类型防护措施不太常用,不过使用这个特性可以简化代码,提升代码的可重用性。如果没有这个特性,要在行内使用typeof和instanceof类型防护措施,而构建isLegacyDialog和isString之类的函数做同样的检查可实现更好的封装,提升代码的可读性。
在Typescript的众多特性中,条件类型算是最独特的。概括的说,条件类型表达的是,“声明一个依赖类型U和V的类型T,如果U<:V,把T赋值给A否则,把T赋值给B”
type IsString = T extends string//1.
? true// 2.
: false// 3.
type A = IsString // true
type B = IsString // false
注意,这里的句法和值层面的三元表达式差不多,只是现在位于类型层面。和三元表达式相似的是,条件类型可以嵌套。
条件类型不限于只能在类型别名中使用,可以使用类型的地方几乎都能使用条件类型。包括类型别名,接口,类,参数的类型,以及函数和方法的泛型默认类型。
这个类型 |
等价于 |
---|---|
string extends T ? A:B |
string extends T ?A:B |
`(string | number)extends T?A:B` |
`(string | number |
type ToArray = T[]
type A = ToArray // number[]
type B = ToArray // (number|string)[]
type ToArray2 = T extends unknown ? T[]:T[]
type A = ToArray2 // number[]
type B = ToArray2 // (number|string)[]
这样做有什么作用呢?可以安全地表达一些常见的操作
我们知道,Typescript内置了计算两个类型交集的&运算符,还内置了计算两个类型并集的|运算符。下面我们来够键Without
type Without = T extends U ? never : T
type A = Without// number|string
下面具体分析Typescript是如何实现的:
type A = Without
type A = Without|Without|Without
type A = (boolean extends boolean?never:boolean)|(number extends boolean?never:boolean)|(string extends boolean?never:boolean)
type A = never|number|string
如果条件类型没有分配性质,最终的结果将是never
条件类型的最后一个特性是可以在条件中声明泛型。在条件类型中声明泛型不使用这个句法,而使用infer关键字。
下面声明一个条件类型ElementTpe,获取数组中元素的类型:
// 获取数组中元素的类型: T[number]索引类型
type ElementType = T extends unknown[] ? T[number] : T
type A = ElementType<(number|string)[]> // string|number
type B = ElementType //string
使用infer关键字可以重写为:
type ElementType2 = T extends (infer U)[] ? U : T
type B = ElementType2 // number
这里,ElementType和ElementType2是等价的。注意,infer子句声明了一个新的类型变量U,Typescript将根据传给ElementType2的T推导U的类。
另外注意,U是在行内声明的,而没有和T一起在前声明。倘若在前面声明,结果如何?
type ElementUgly = T extends U[] ? U : T;
type C = ElementUgly// 需要两个参数
更加复杂的例子
type SecondArg =
F extends (a: any, b: infer B) => any ? B : never
// 获取Array.slice的类型
type F = typeof Array['prototype']['slice']
type A = SecondArg // number|undefined
可见,[].slice的第二个参数是number|undefined类型。而且在编译时便可知晓这一点。
利用条件类型可以在类型层面表达一些强大的操作,Typescript自带了一些全局可用的条件类型
和前面的Without类型,计算在T中而不在U中的类型:
type A = number|string type B = string type C = Exclude// number
计算T中可赋值给U的类型:
type A = number|string type B = string type C = Extract // string
从T中排除null和undefined
type A = {a?:number|null} type B = NonNullable// number
计算函数的返回类型(注意,不适用于泛型和重载的函数)
type F = (a:number) = string type R = ReturnType
// string
计算类构造方法的实例类型
type A = {new():B} type B = {b:number} type I = InstanceType // {b:number}
type A = { new():{b:number} } class B { b:number; constructor(){ this.b = 123 } } let b:A = B let c:B = new B() console.log(c);
和前面的Without类型,计算在T中而不在U中的类型:
type A = number|string type B = string type C = Exclude// number
有时候,我们没有足够的时间把所有类型都规划好,这时我们希望Typescript能相信我们,即便如此也是安全的。还有我们是从API中获取数据,而暂时还没有生成类型声明。
Typescript提供了大量的特性,但是要少用。
给定类型B,如果A<:B<:C
,那么我们可以向类型检查器断定,B其实是A或C。注意:我们只能断定一个类型是自身的超类型或子类型,不能断定number是string,因为这两个类型毫无关系。
Typescript为类型断言提供了两种句法:
function formatInput(input:string){
//
}
function getUserInput():string|number{
return ""
}
let input = getUserInput()
formatInput(input as string)// 1.
formatInput(input)//2.
优先使用as,<>尖括号和react中tsx语法冲突
有时候,两个类型之间关系不明,无法断定具体类型,可以使用as any,
显然,类型断言不安全,少用
可为空的类型,即T|null或T|null|undefined类型,比较特殊,Typescript为此提供了专门的语法,用于断定类型为T,而不是null或undefined。
type Dialog = {
id?:string
}
function closeDialog(dialog:Dialog){
if(!dialog.id){// 1.
return
}
setTimeout(()=>{// 2.
removeFromDOM(
dialog,
document.getElementById(dialog.id) // 错误
)
})
}
function removeFromDOM(dialog:Dialog,element:Element){
element.parentNode.removeChild(element) // 4.
delete dialog.id
}
Typescript提供特殊的句法!,确定不可能null|undefined
type Dialog = {
id?:string
}
function closeDialog(dialog:Dialog){
if(!dialog.id){// 1.
return
}
setTimeout(()=>{// 2.
removeFromDOM(
dialog,
document.getElementById(dialog.id!)! // 错误
)
})
}
function removeFromDOM(dialog:Dialog,element:Element){
element.parentNode!.removeChild(element) // 4.
delete dialog.id
}
如果频繁使用非空断言,说明代码需要重构。
type VisibleDialog = { id?: string }
type DestroyDialog = {}
type Dialog = VisibleDialog | DestroyDialog
function closeDialog(dialog: Dialog) {
if (!("id" in dialog)) {
return
}
setTimeout(() => {
removeFromDOM(
dialog,
document.querySelector("#" + dialog.id)!
)
});
}
function removeFromDOM(dialog: VisibleDialog, element: Element) {
if(element.parentElement){
element.parentElement.removeChild(element)
if( dialog.id ){
delete dialog.id
}
}
}
确认dialog有id属性之后(表明是VisibleDialog类型),即使在箭头函数中Typescript也知道dialog引用没有变化:箭头函数与外面的dialog相同,因此结果细化随之转移,不会像前例那样不起作用。
Typescrip为非空断言提供了专门的句法,用于检查有没有明确赋值。
我们可以使用明确赋值断言告诉Typescript,使用!
let userId!:string// 表明已经明确赋值
fetchUser()
userId.toUpperCase() // OK
function fetchUser(){
userId = globalCache.get('userId')
}
与类型断言和非空断言一样,如果经常使用明确赋值断言,可能表示你的代码有问题。
type CompanyId = string
type OrderId = string
type UserId = string
type Id = CompanyId|OrderId|UserId
function queryForUser(id:UserId){
//
}
let id:CompanyId = "ge3633ghg"
queryForUser(id);//ok!!!
这时就体现名义类型的作用了。虽然Typescript不支持名义类型,但是我们可以使用类型烙印(type branding)技术模拟实现。使用类型烙印技术之前要稍微设置一下。
首先,为各个名义类型合成类型烙印:
type CompanyId = string & {readonly brand:unique symbol}
type OrderId = string & {readonly brand:unique symbol}
type UserId = string & {readonly brand:unique symbol}
type Id = CompanyId|OrderId|UserId
function CompanyId(id:string){
return id as CompanyId
}
function OrderId(id:string){
return id as OrderId
}
function UserId(id:string){
return id as UserId
}
function queryForUser(id:UserId){
//
}
let companyId = CompanyId("dgge")
let orderId = OrderId("geg8g8eg")
let userId = UserId("dgegeg")
queryForUser(userId)//ok
queryForUser(companyId)// error
这种方式的优点是,降低了运行时的开销,没构建一个ID只需要调用一个函数,而且JavaScriptVM还有可能把函数放在行内。在运行时,一个ID就是一个字符串,烙印纯粹是一种编译时结构。
同样,大多时候,没必要使用,烙印可以提高安全性。
构建JavaScript时候,传统的观点是扩展内置的类型的原型不安全。
虽然过去认为扩展原型不安全,但是有了Typescript提供的静态类型系统,可以放心扩展。
function tuple<
T extends unknown[] // 2.
>(...ts:T):T{
return ts
}
interface Array {//1.
zip(list: U[]): [T, U][],
}
Array.prototype.zip = function (
this: T[],//2.
list: U[]
): [T,U][] {
return this.map((v, k) => {
return tuple(v, list[k]) // 3.
})
};
console.log([1,2,2].map(n=>n*2).zip(['a','b','c']));//[ [ 2, 'a' ], [ 4, 'b' ], [ 4, 'c' ] ]
注意:我们声明的interface Array是对全局命名空间Array的增强,影响整个Typescript,即使没有导入文件,Typescript看来,[].zip方法也可用,为了增强Array.prototype,我们需要确保用到zip方法的文件都已经加载过了上面代码(zip.ts),这样才能让Array.prototype上的zip方法生效。
编辑tsconfig.json,把zip.ts排除在项目之外,这样使用方必须先import导入:
{
*exclude*:[
"./zip.ts"
]
}
现在可以使用zip方法了
import "./zip"
console.log([1,2,2].map(n=>n*2).zip(['a','b','c']));