建站高端网站软件下载网站哪个好用
- 作者: 五速梦信息网
- 时间: 2026年04月20日 10:40
当前位置: 首页 > news >正文
建站高端网站,软件下载网站哪个好用,单页网站制作教程,哪些网站上可以做租车文章目录 第六章 类型进阶6.1 类型之间的关系6.1.1 子类型和超类型6.1.2 型变结构和数组型变函数型变 6.1.3 可赋值性6.1.4 类型拓宽const类型多余属性检查 6.1.5 细化辨别并集类型 6.2 全面性检查6.3对象类型进阶6.3.1 对象类型的类型运算符“键入”运算符keyof运算符 6.3.2 R… 文章目录 第六章 类型进阶6.1 类型之间的关系6.1.1 子类型和超类型6.1.2 型变结构和数组型变函数型变 6.1.3 可赋值性6.1.4 类型拓宽const类型多余属性检查 6.1.5 细化辨别并集类型 6.2 全面性检查6.3对象类型进阶6.3.1 对象类型的类型运算符“键入”运算符keyof运算符 6.3.2 Record类型6.3.3 映射类型内置的映射类型RecordKeys,ValuesPartialObjectRequiredObjectReadonlyObjectPickObject,Keys 6.3.4 伴生对象模式 6.4 函数类型进阶6.4.1 改善元组的类型推导6.4.2 用户定义的类型防护措施 6.5 条件类型6.5.1 条件分配6.5.2 infer关键字6.5.3 内置的条件类型ExcludeT,UExtractT,UNonNullableTReturnTypeFInstanceTypeCExcludeT,U 6.6 解决方法6.6.1 类型断言6.6.2 非空断言6.6.3 明确赋值断言 6,7 模拟名义类型隐含类型opaque type6.8 安全的扩展原型 第六章 类型进阶
Typescript一流的类型系统支持强大的类型层面编程特性Typescript的类型系统不仅具有极强的表现力易于使用个而且可通过简介明了的方式声明类型约束和关系并且多数时候能够自动为我们推导。
本节首先讨论Typescript中的子类型可赋值性型变和类型拓展加深你对前几章的认识。然后深入说明Typescript基于控制流的类型检查特性包括类型细化和全面性检查。
接下来讨论类型层面的一些高级编程特性“键入”和映射对象类型使用条件类型自定义类型防护措施以及类型断言和明确赋值断言等。
最后介绍一些高级模式尽量提升类型的安全性伴生对象模式改善元组的类型推导模拟名义类型以及安全扩展原型的方式。
6.1 类型之间的关系
首先讨论Typescript中的类型关系
6.1.1 子类型和超类型
3.1节讲过可赋值性。我们已经了解了Typescript提供的多数类型现在可以深挖一些细节。 子类型给定两个类型A和B假设B是A的子类型那么在需要A的地方都可以放心使用B Array是Object的子类型Tuple是Array的子类型所有类型都是any的子类型never是所有类型的子类型如果Bird类扩展自Animal类那么Bird是Animal的子类型
超类型正好和子类型相反
6.1.2 型变 多数时候很容易判断A类型是不是B类型的子类型。例如对numberstring等类型来说number包含在并集类型number|string中那么number必定是他的子类型 但是对参数化类型泛型和其他较为复杂的类型来说情况不那么明晰。
什么情况下Array是Array的子类型什么情况下结构A是结构B的子类型什么情况下函数(a:A)B是函数(c:C)D的子类型
如果一个类型中包含其他类型即带有类型参数的类型如Array;带有字段的结构如{a:number};或者函数如(a:A)B,使用上述规则很难判断谁是子类型。
为了便于理解本书作者引入了一套句法以便使用简介且准确的语言讨论类型。这套句法不是有效的Typescript代码只是为了使用一套语言讨论类型。
A:B指 “A类型是B类型的子类型或者为同类类型”A:B指“A类型是B类型的超类型或者为同类类型”
结构和数组型变
type ExistingUser {id:numbername: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:numbername:string
}
type NewUser {name:string
}
type LegacyUser {id?:number|stringname: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: numberdeleteUser(legacyUser)// 报错我们传入的结构中有一个属性的类型是预期类型的超类型Typescript报错了这是因为id的类型是string|number|undefined,而deleteUser函数只处理了number|undefined的情况。 Typescript的行为是这样的:对预期的结构还可以使用属性的类型:预期类型的结构但是不能传入属性的类型是预期类型的超类型的结构。 在类型上我们说Typescript对结构对象和类的属性类型进行了 协变covariant。 也就是说如果想保证A对象可赋值给B对象那么A对象的每个成员都必须:B对象的对应属性。
其实协变只是型变的四种方式之一
不变 只能T 协变 可以是:T 逆变 可以是:T 双变 可以是:T或:T 在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的子类型
函数A的this类型未指定或者:函数B的this类型函数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 Birdlet 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的类型:另一个函数相应参数的类型。 我们不用记诵这些规则代码编辑器能够很好的提示。
6.1.3 可赋值性 子类型和超类型关系是静态类型语言的核心概念对理解可赋值性也十分重要可赋值性指在判断需要B类型的地方可否使用A类型时才用的规则。 如果A是B的子类型那么需要B的地方也可以使用AA是any
6.1.4 类型拓宽 类型扩宽type widening是理解Typescript类型推导机制的关键。一般来说Typescript在推导类型时会方宽要求故意推导一个更宽泛的类型而不限定为某个具体的类型。 声明变量时如果允许以后修改变量的值letvar变量的类型将拓宽从字面值放大到包含该字面量的基类型
let a x // stringvar 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 // anyconst类型
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:stringcacheSize?:numbertier?:prod|dev
}
class API {constructor(private options:Options){}
}
new API({baseURL:xxxx,tier:prod
})
// 如果有一个参数拼错了
new API({baseURL:xxxtierr:prod
})// 直接报错这是编写JavaScript代码经常遇到的问题Typescript能够捕获这种问题。可是对象类型的成员会做协变Typescript是如何捕获这种问题的呢
过程是这样
预期的类型{baseURL:string,cacheSize?:number,tier?:’prod’|‘dev’}/传入的类型{baseURL:string,tierr:string}传入的类型是预期类型的子类型可是不知为何Typescript知道要报告错误。
Typescript之所以能够捕获这样的问题是因为他会做多余属性检查具体过程是尝试把一个新鲜对象字面量类型fresh object literal typeT赋值给另一个类型U时如果T有不在U中的属性Typescript将报错。 新鲜对象字面量类型指的是Typescript从对象字面量中推导出来的类型。 如果对象字面量有类型断言6.6.1节或者把对象字面量赋值给变量那么新鲜字面量类型将扩宽为常规的对象类型也就不能称其为新鲜对象字面量类型。 6.1.5 细化
Typescript才用的基于流的类型推导这是一种符号执行类型检查器子啊检查代码过程中利用流程语句if||和switch和类型查询如typeofinstanceof和in细化类型。这是一个极其便利的特性但是很少有语言支持。
type Unit cm|px|%let units:Unit[] [cm,px,%]// 检查各个单位如果没有匹配返回null
function parseUnit(value:string):Unit|null{for(let i0;iunits.length;i){if(value.endsWith(units[i])){return units[i]}}return null
}我们可以使用parseUnit函数解析用户传入的宽度值。width的值可能是一个数字此时假定单位为像素可能是一个带单位的字符串也可能是null或undefined。
下述代码对象类型做了多次细化
type Unit cm|px|%let units:Unit[] [cm,px,%]// 检查各个单位如果没有匹配返回null
function parseUnit(value:string):Unit|null{for(let i0;iunits.length;i){if(value.endsWith(units[i])){return units[i]}}return null
}type Width {unit:Unitvalue:number
}function parseWidth(width:number|string|null|undefined):Width|null{// 如果width是null或undefined直接返回if(widthnull){// 1.return null}// 如果width是一个数字默认单位为像素if(typeof width number){// 2.return {unit:px,value:width}}// 尝试从width中解析出单位let unit parseUnit(width)if(unit){// 3.return {unit,value:parseFloat(width)}}return null // 4.
}与null做不严格相等的等值检查便能在遇到JavaScript值null和undefined返回true。width的类型变成string|numbertypeof运算符查询值的类型。width的类型变成stringif如果为真表示有单位否则必为null返回null。 JavaScript有七个假值null,undefined,NaN,0,-0,””和false。其他均为真值 辨别并集类型
Typescript能跟随你的脚步细化类型
type UserTextEvent {value:string}
type UserMouseEvent {value:[number,number]}type UserEvent UserTextEvent|UserMouseEventfunction handle(event:UserEvent){if(typeof event.value string){event.value // string// do somethingreturn}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|UserMouseEventfunction handle(event:UserEvent){if(typeof event.value string){event.value // stringevent.target // HTMLInputElement | HTMLElement!!!// do somethingreturn}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|UserMouseEventfunction handle(event:UserEvent){if(event.type TextEvent){event.value // stringevent.target // HTMLInputElement// do somethingreturn}event.value // [number,number]event.target // HTMLElement!!!
}现在根据标记字段event.type)的值细化event,Typescript知道在if分支中event必为UserTextEvent类型。由于标记是独一无二的所以Typescript知道二者时互斥的。
如果函数要处理并集类型的不同情况应该使用标记。例如在处理Flux动作Redux规约器或React的useReducer时其作用十分巨大。
6.2 全面性检查 全面性检查也称穷尽性检查是类型检查器所做的一项检查为的是确保所有情况被覆盖了。这个概念源自模式匹配的语言例如Haskell,OCaml等。 tsconfig.json noImplictReturns将提示没有return Typescript在很多情况下都会做全面性检查如果发现缺少某种情况会提醒你例如
type Weekday Mon|Tue|Wed|Thu|Fri
type Day Weekday | Sat | Sunfunction 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(n100){return true}}6.3对象类型进阶
对象是JavaScript的核心为了以安全的方式描述和处理对象Typescript提供了一系列方式。
6.3.1 对象类型的类型运算符
还记得“并集类型和交集类型”一节介绍的|和类型运算符。Typescript不只这两个。下面再介绍几个处理对象结构的类型运算符。
“键入”运算符
假设有个复杂的嵌套类型描述从社交媒体API中得到的GraphQL API相应
type APIResponse {user:{userId:stringfriendList:{count:numberfriends:{firstName:stringlastName:string}[]}}
}
function getAPIResponse():PromiseAPIResponse{return Promise.resolveAPIResponse({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运算符 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润函数的参数为一个对象O和一个键Kkeyof O是一个字符串字面量类型并集表示o对象所有键。K类扩展这个并集是该并集的子类型。O[K]的类型为在O中查找K得到的具体类型。接着2.说如果K是‘name’那么在编译时get返回一个字符串如果K是’age‘|’ok‘那么get返回number|boolean.
这两个类型运算符强大在于可以准确安全的描述结构类型。。
type ActiveityLog {lastEvent: Dateevents: {id: stringtimestamp: Datetype: 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 extends object, K1 extends keyof O(o: O, k1: K1): O[K1]O extends object, K1 extends keyof O, K2 extends keyof OK1: O[K1][K2]O extends object, K1 extends keyof O, K2 extends keyof O[K1], K3 extends keyof O[K1]K2: O[K1][K2][K3]
}let get: Get (object: any, …keys: string[]) {let result objectkeys.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 }// 1236.3.2 Record类型
Typescript内置的Record类型用于描述有映射关系的对象。
type Obj {name:string,age:number,gender:boolean
}
let a:keyof Obj name
console.log(a);let b:Recordkeyof Obj,Obj[keyof Obj] {name:ddd,age:15,gender:false
}
console.log(b);// { name: ddd, age: 15, gender: false }
let c:keyof typeof b name
console.log©;// name与常规的对象索引签名相比Record提供了更多的便利使用常规的索引签名可以约束对象中值的类型不过键只能用stringnumber或symbol类型使用Record还可以约束对象的键为string和number的子类型。
6.3.3 映射类型
Typescript还提供了一种更强大的方式即映射类型mapped type)使用这种方式声明更安全。
type Weekday Mon|Tue|Wed|Thu|Fri
type Day Weekday | Sat | Sunlet nextDay:{[K in Weekday]:Day} {Mon:Tue,Tue:Wed,Wed:Thu,Thu:Fri,Fri:Sat,
}映射类型使用独特的句法。与索引签名相同一个对象最多有一个映射类型
type MyMappedType {[key in UnionType]:ValueType
}其实Typescript内置的Record类型也是使用映射类型实现的
type RecordK extends keyof any,T {[P in K]:T
}映射类型的功能比Record强大在指定对象的键和值的类型以外如果结合”键入“类型还能约束特定键和值是什么类型。
下面举几个例子说明使用映射类型可以做哪些事
type Account {id:numberisEmployee:booleannotes: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.
}新建对象类型OptionalAccount,与Account建立映射在此过程中把各个字段标记为可选的。新建对象类型NullableAccount,与Account建立映射在此过程中为每个字段增加可选值null。新建对象类型ReadonlyAccount与Account建立映射把各字段标记为只读即可读不可写字段可以标记为可选或只读readonly也可以把这个约束去掉。使用减号-运算符一个特殊的运算符只对映射类型可用可以把和readonly撤销分别把字段还原为必须得和可写的。这里新建一个对象类型Account2,与ReadonlyAccount建立映射使用减号(-)运算符把readonly修饰符去掉最终得到的类型等同于Account.新建对象类型Account3,与OptionalAccount建立映射使用减号-运算符可以把可选运算符去掉最终得到的类型等同于Account. 减号-运算符有个对应的运算符。一般不直接使用加好运算符因为它通常蕴含在其他运算符中。在映射类型中readonly等效于readonly等效于、的存在只是为了确保整体协调。 内置的映射类型
前一节讨论的映射类型非常有用Typescript内置了一些
RecordKeys,Values 键的类型为Keys值的类型为Values的对象。 PartialObject 把Object中的每个字段都标记为可选的 RequiredObject 把Object中的每个字段都标记为必须得 ReadonlyObject 把Object中的每个字段都标记为只读的 PickObject,Keys 返回Object的子类型只含指定的Keys 6.3.4 伴生对象模式
伴生对象源自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:stringtest(name:string):boolean
}
let Demo {name:demo,test(name:Demo[name]):boolean {return false},
}
Demo.test(a)Typescript中的类型和值分别在不同的命名空间中。10.4节将深入说明。这意味着在同一个作用域中可以有同名这里的Currency)的类型和值。伴生对象模式在彼此独立的命名空间中两次声明相同的名称一个是类型另一个是值。
这种模式有几个不错的性质。首先可以把语义上归属为同一名称的类型和值放在一起。其次使用方可以一次性导入二者。
import {Currency} from ./Currencylet amountDue:Currency {// 1.unit:JPY,value:23344
}
let otherAmountDue Currency.from(330,EUR)// 2.使用的类型是Currency使用的是值Currency
如果一个类型和一个对象在语义上 有关联就可以使用伴生对象模式由对象提供操作类型的使用方法。
6.4 函数类型进阶
本节讲述函数类型常用的几种高级技术。
6.4.1 改善元组的类型推导
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]声明tuple函数用于构建元组类型代替内置的[]语法声明一个类型参数T他是unknown[]的子类型表明T是任意类型的数组tuple函数接受不定数量的参数ts.由于T描述的是剩余参数因此Typescript导出的一个元组类型。tuple函数的返回类型与ts的推导结果相同这个函数返回传入的参数。神奇之处全在类型中。
6.4.2 用户定义的类型防护措施
在某些情况下比如函数的返回值只说函数返回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:stringif(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:stringif(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之类的函数做同样的检查可实现更好的封装提升代码的可读性。
6.5 条件类型
在Typescript的众多特性中条件类型算是最独特的。概括的说条件类型表达的是“声明一个依赖类型U和V的类型T如果U:V把T赋值给A否则把T赋值给B”
type IsStringT T extends string//1.? true// 2.: false// 3.
type A IsStringstring // true
type B IsStringnumber // false声明一个函数有一个泛型T。这个条件类型中的“条件”时T extends string,即“T是不是string的子类型”如果T是string的子类型得到的类型是true否则得到的类型为false
注意这里的句法和值层面的三元表达式差不多只是现在位于类型层面。和三元表达式相似的是条件类型可以嵌套。
条件类型不限于只能在类型别名中使用可以使用类型的地方几乎都能使用条件类型。包括类型别名接口类参数的类型以及函数和方法的泛型默认类型。
6.5.1 条件分配
这个类型等价于string extends T ? A:Bstring extends T ?A:B(stringnumber)extends T?A:B(stringnumber
type ToArrayT T[]
type A ToArraynumber // number[]
type B ToArraynumber|string // (number|string)[]type ToArray2T T extends unknown ? T[]:T[]
type A ToArray2number // number[]
type B ToArray2number|string // (number|string)[]这样做有什么作用呢可以安全地表达一些常见的操作
我们知道Typescript内置了计算两个类型交集的运算符还内置了计算两个类型并集的|运算符。下面我们来够键WithoutT,U计算在T中而不在U中的类型
type WithoutT, U T extends U ? never : Ttype A Withoutboolean|number|string,boolean// number|string下面具体分析Typescript是如何实现的
先分析输入的类型type A Withoutboolean|number|string,boolean把条件分配到并集中type A Withoutboolean,boolean|Withoutnumber,boolean|Withoutstring,boolean带入Without定义替换T和U:type A (boolean extends boolean?never:boolean)|(number extends boolean?never:boolean)|(string extends boolean?never:boolean)计算条件type A never|number|string化简type A number|string
如果条件类型没有分配性质最终的结果将是never
6.5.2 infer关键字
条件类型的最后一个特性是可以在条件中声明泛型。在条件类型中声明泛型不使用这个句法而使用infer关键字。
下面声明一个条件类型ElementTpe获取数组中元素的类型
// 获取数组中元素的类型 T[number]索引类型
type ElementTypeT T extends unknown[] ? T[number] : T
type A ElementType(number|string)[] // string|number
type B ElementTypestring //string使用infer关键字可以重写为
type ElementType2T T extends (infer U)[] ? U : T
type B ElementType2number[] // number这里ElementType和ElementType2是等价的。注意infer子句声明了一个新的类型变量UTypescript将根据传给ElementType2的T推导U的类。
另外注意U是在行内声明的而没有和T一起在前声明。倘若在前面声明结果如何
type ElementUglyT,U T extends U[] ? U : T;
type C ElementUglynumber[]// 需要两个参数更加复杂的例子
type SecondArgF
F extends (a: any, b: infer B) any ? B : never// 获取Array.slice的类型
type F typeof Array[prototype][slice]
type A SecondArgF // number|undefined可见[].slice的第二个参数是number|undefined类型。而且在编译时便可知晓这一点。
6.5.3 内置的条件类型
利用条件类型可以在类型层面表达一些强大的操作Typescript自带了一些全局可用的条件类型
ExcludeT,U 和前面的Without类型计算在T中而不在U中的类型 type A number|string
type B string
type C ExcludeA,B// numberExtractT,U 计算T中可赋值给U的类型 type A number|string
type B string
type C ExtractA,B // stringNonNullable 从T中排除null和undefined type A {a?:number|null}
type B NonNullableA[a]// numberReturnType 计算函数的返回类型(注意不适用于泛型和重载的函数) type F (a:number) string
type R ReturnTypeF // stringInstanceType 计算类构造方法的实例类型 type A {new():B}
type B {b:number}
type I InstanceTypeA // {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©;ExcludeT,U 和前面的Without类型计算在T中而不在U中的类型 type A number|string
type B string
type C ExcludeA,B// number6.6 解决方法
有时候我们没有足够的时间把所有类型都规划好这时我们希望Typescript能相信我们即便如此也是安全的。还有我们是从API中获取数据而暂时还没有生成类型声明。 Typescript提供了大量的特性但是要少用。 6.6.1 类型断言
给定类型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(stringinput)//2.使用断言as)告诉Typescriptinput是字符串而不是string|number类型。如果想先测试formatInput函数而且肯定getUserInput函数返回一个字符串就可以这么做类型断言的旧句法使用尖括号。这两种句法是相同的作用。 优先使用as尖括号和react中tsx语法冲突 有时候两个类型之间关系不明无法断定具体类型可以使用as any
显然类型断言不安全少用
6.6.2 非空断言
可为空的类型即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
}如果没有id就返回在下一次时间循环时删除对话框身处一个箭头函数中开始一个新作用域Typescript不知道1.和2.之间的代码修饰了dialog因此1.中所做的类型细化不起作用。类似的尽管我们知道对话框一定在dom中而且绝对有父级dom然而Typescript只知道element.parentNode的类型是Node|null
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相同因此结果细化随之转移不会像前例那样不起作用。
6.6.3 明确赋值断言
Typescrip为非空断言提供了专门的句法用于检查有没有明确赋值。
我们可以使用明确赋值断言告诉Typescript使用
let userId!:string// 表明已经明确赋值
fetchUser()
userId.toUpperCase() // OK
function fetchUser(){userId globalCache.get(userId)
}与类型断言和非空断言一样如果经常使用明确赋值断言可能表示你的代码有问题。
6,7 模拟名义类型隐含类型opaque type
type CompanyId string
type OrderId string
type UserId string
type Id CompanyId|OrderId|UserIdfunction 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|UserIdfunction 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就是一个字符串烙印纯粹是一种编译时结构。
同样大多时候没必要使用烙印可以提高安全性。
6.8 安全的扩展原型
构建JavaScript时候传统的观点是扩展内置的类型的原型不安全。
虽然过去认为扩展原型不安全但是有了Typescript提供的静态类型系统可以放心扩展。
function tupleT extends unknown[] // 2.
(…ts:T):T{return ts
}
interface ArrayT {//1.zipU(list: U[]): [T, U][],
}Array.prototype.zip function T, U(this: T[],//2.list: U[]
): [T,U][] {return this.map((v, k) {return tuple(v, list[k]) // 3.})
};
console.log([1,2,2].map(nn*2).zip([a,b,c]));//[ [ 2, a ], [ 4, b ], [ 4, c ] ]首先让Typescript知道我们要为Array添加zip方法。我们利用接口合并特性5.4.1节增强全局接口Array,为这个全局定义的接口添加zip方法。这个文件没有显示导出或导入意味着在脚本模式10,2,3节因此可以直接增强全局接口Array。我们声明一个接口与现有的Array同名Typescript负责将二者合并。如果文件在模块模式中需要导入其他代码便是这种情况就要把全局扩展放在declare global类型声明中11.1节global是一个特殊的命名空间包含所有全局定义的值在模块模式中无需导入就能使用任何值见第十章可以增强模块模式文件中全局作用域内的名称。然后在Array的原型上实现zip方法。这里使用this类型以便让Typescript正确推导出调用zip方法的数组的类型T由于Typescript推导出的映射函数的返回类型是(T|U)[]Typescript没那么智能意识不到这个元组的0索引始终是T1索引始终是U)所以我们使用tuple函数6.4.1节创建一个元组类型而不使用类型断言。
注意我们声明的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(nn*2).zip([a,b,c]));
- 上一篇: 建站多少钱一个个人开通微信小程序
- 下一篇: 建站工具 开源建站服务搭建的页面时
相关文章
-
建站多少钱一个个人开通微信小程序
建站多少钱一个个人开通微信小程序
- 技术栈
- 2026年04月20日
-
建站报价自己用电脑做网站服务器
建站报价自己用电脑做网站服务器
- 技术栈
- 2026年04月20日
-
建站宝盒小程序网站建设设计制作方案与价格
建站宝盒小程序网站建设设计制作方案与价格
- 技术栈
- 2026年04月20日
-
建站工具 开源建站服务搭建的页面时
建站工具 开源建站服务搭建的页面时
- 技术栈
- 2026年04月20日
-
建站工具 开源微信公众号登录怎么退出
建站工具 开源微信公众号登录怎么退出
- 技术栈
- 2026年04月20日
-
建站工具帝国网站大幅广告
建站工具帝国网站大幅广告
- 技术栈
- 2026年04月20日
