当前位置: 首页 > news >正文

Typescript第六章 类型进阶(类型之间的关系,全面性检查,对象类型进阶,函数类型进阶,条件类型等)

文章目录

  • 第六章 类型进阶
    • 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 映射类型
        • 内置的映射类型
          • `Record<Keys,Values>`
          • `Partial<Object>`
          • `Required<Object>`
          • `Readonly<Object>`
          • `Pick<Object,Keys>`
      • 6.3.4 伴生对象模式
    • 6.4 函数类型进阶
      • 6.4.1 改善元组的类型推导
      • 6.4.2 用户定义的类型防护措施
    • 6.5 条件类型
      • 6.5.1 条件分配
      • 6.5.2 infer关键字
      • 6.5.3 内置的条件类型
        • Exclude<T,U>
        • Extract<T,U>
        • NonNullable<T>
        • ReturnType<F>
        • InstanceType<C>
        • Exclude<T,U>
    • 6.6 解决方法
      • 6.6.1 类型断言
      • 6.6.2 非空断言
      • 6.6.3 明确赋值断言
    • 6,7 模拟名义类型(隐含类型(opaque type))
    • 6.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类型的子类型。例如:对number,string等类型来说,(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的子类型:

  1. 函数A的this类型未指定,或者>:函数B的this类型
  2. 函数A的各个参数的类型>:函数B的相应参数。
  3. 函数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类型时才用的规则)。

  1. 如果A是B的子类型,那么需要B的地方也可以使用A
  2. A是any

6.1.4 类型拓宽

类型扩宽(type widening)是理解Typescript类型推导机制的关键。一般来说,Typescript在推导类型时会方宽要求,故意推导一个更宽泛的类型,而不限定为某个具体的类型。

声明变量时如果允许以后修改变量的值(let,var),变量的类型将拓宽,从字面值放大到包含该字面量的基类型

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 // any

const类型

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:"xxx"tierr:"prod"
})// 直接报错

这是编写JavaScript代码经常遇到的问题,Typescript能够捕获这种问题。可是,对象类型的成员会做协变,Typescript是如何捕获这种问题的呢?

过程是这样

  • 预期的类型{baseURL:string,cacheSize?:number,tier?:’prod’|‘dev’}/
  • 传入的类型{baseURL:string,tierr:string}
  • 传入的类型是预期类型的子类型,可是不知为何,Typescript知道要报告错误。

Typescript之所以能够捕获这样的问题,是因为他会做多余属性检查,具体过程是:尝试把一个新鲜对象字面量类型(fresh object literal type)T赋值给另一个类型U时,如果T有不在U中的属性,Typescript将报错。

新鲜对象字面量类型指的是Typescript从对象字面量中推导出来的类型。

如果对象字面量有类型断言(6.6.1节),或者把对象字面量赋值给变量,那么新鲜字面量类型将扩宽为常规的对象类型,也就不能称其为新鲜对象字面量类型。

6.1.5 细化

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<units.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 i=0;i<units.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(width==null){// 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.
}
  1. 与null做不严格相等的等值检查便能在遇到JavaScript值null和undefined返回true。width的类型变成string|number
  2. typeof运算符查询值的类型。width的类型变成string
  3. if如果为真表示有单位,否则必为null
  4. 返回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' | '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}}

6.3对象类型进阶

对象是JavaScript的核心,为了以安全的方式描述和处理对象,Typescript提供了一系列方式。

6.3.1 对象类型的类型运算符

还记得“并集类型和交集类型”一节介绍的|和&类型运算符。Typescript不只这两个。下面再介绍几个处理对象结构的类型运算符。

“键入”运算符

假设有个复杂的嵌套类型,描述从社交媒体API中得到的GraphQL API相应

type APIResponse = {user:{userId:stringfriendList:{count:numberfriends:{firstName:stringlastName:string}[]}}
}
function getAPIResponse():Promise<APIResponse>{return Promise.resolve<APIResponse>({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润
  1. 函数的参数为一个对象O和一个键K
  2. keyof O是一个字符串字面量类型并集,表示o对象所有键。K类扩展这个并集(是该并集的子类型。
  3. 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 O[K1]>(o: O, k1: K1, k2: K2): O[K1][K2]<O extends object, K1 extends keyof O, K2 extends keyof O[K1], K3 extends keyof O[K1][K2]>(o: O, k1: K1, k2: K2, k3: K3): 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' }// 123

6.3.2 Record类型

Typescript内置的Record类型用于描述有映射关系的对象。

type Obj = {name:string,age:number,gender:boolean
}
let a:keyof Obj = 'name'
console.log(a);let b:Record<keyof 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(c);// name

与常规的对象索引签名相比,Record提供了更多的便利:使用常规的索引签名可以约束对象中值的类型,不过键只能用string,number,或symbol类型;使用Record还可以约束对象的键为string和number的子类型。

6.3.3 映射类型

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<K 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.
}
  1. 新建对象类型OptionalAccount,与Account建立映射,在此过程中把各个字段标记为可选的。
  2. 新建对象类型NullableAccount,与Account建立映射,在此过程中为每个字段增加可选值null。
  3. 新建对象类型ReadonlyAccount,与Account建立映射,把各字段标记为只读(即可读不可写)
  4. 字段可以标记为可选(?)或只读(readonly),也可以把这个约束去掉。使用减号(-)运算符(一个特殊的运算符,只对映射类型可用)可以把?和readonly撤销,分别把字段还原为必须得和可写的。这里新建一个对象类型Account2,与ReadonlyAccount建立映射,使用减号(-)运算符把readonly修饰符去掉,最终得到的类型等同于Account.
  5. 新建对象类型Account3,与OptionalAccount建立映射,使用减号(-)运算符可以把可选(?)运算符去掉,最终得到的类型等同于Account.

减号(-)运算符有个对应的(+)运算符。一般不直接使用加好运算符,因为它通常蕴含在其他运算符中。在映射类型中,readonly等效于+readonly,?等效于+?、+的存在只是为了确保整体协调。

内置的映射类型

前一节讨论的映射类型非常有用,Typescript内置了一些

Record<Keys,Values>

键的类型为Keys,值的类型为Values的对象。

Partial<Object>

把Object中的每个字段都标记为可选的

Required<Object>

把Object中的每个字段都标记为必须得

Readonly<Object>

把Object中的每个字段都标记为只读的

Pick<Object,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 './Currency'let amountDue:Currency ={// 1.unit:'JPY',value:23344
}
let otherAmountDue = Currency.from(330,"EUR")// 2.
  1. 使用的类型是Currency
  2. 使用的是值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]
  1. 声明tuple函数,用于构建元组类型(代替内置的[]语法)
  2. 声明一个类型参数T,他是unknown[]的子类型(表明T是任意类型的数组)
  3. tuple函数接受不定数量的参数ts.由于T描述的是剩余参数,因此Typescript导出的一个元组类型。
  4. tuple函数的返回类型与ts的推导结果相同
  5. 这个函数返回传入的参数。神奇之处全在类型中。

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 IsString<T> = T extends string//1.? true// 2.: false// 3.
type A = IsString<string> // true
type B = IsString<number> // false
  1. 声明一个函数,有一个泛型T。这个条件类型中的“条件”时T extends string,即“T是不是string的子类型?”
  2. 如果T是string的子类型,得到的类型是true
  3. 否则,得到的类型为false

注意,这里的句法和值层面的三元表达式差不多,只是现在位于类型层面。和三元表达式相似的是,条件类型可以嵌套。

条件类型不限于只能在类型别名中使用,可以使用类型的地方几乎都能使用条件类型。包括类型别名,接口,类,参数的类型,以及函数和方法的泛型默认类型。

6.5.1 条件分配

这个类型等价于
string extends T ? A:Bstring extends T ?A:B
`(stringnumber)extends T?A:B`
`(stringnumber
type ToArray<T> = T[]
type A = ToArray<number> // number[]
type B = ToArray<number|string> // (number|string)[]type ToArray2<T> = T extends unknown ? T[]:T[]
type A = ToArray2<number> // number[]
type B = ToArray2<number|string> // (number|string)[]

这样做有什么作用呢?可以安全地表达一些常见的操作

我们知道,Typescript内置了计算两个类型交集的&运算符,还内置了计算两个类型并集的|运算符。下面我们来够键Without<T,U>,计算在T中而不在U中的类型:

type Without<T, U> = T extends U ? never : Ttype A = Without<boolean|number|string,boolean>// number|string

下面具体分析Typescript是如何实现的:

  1. 先分析输入的类型:type A = Without<boolean|number|string,boolean>
  2. 把条件分配到并集中:type A = Without<boolean,boolean>|Without<number,boolean>|Without<string,boolean>
  3. 带入Without定义,替换T和U:type A = (boolean extends boolean?never:boolean)|(number extends boolean?never:boolean)|(string extends boolean?never:boolean)
  4. 计算条件:type A = never|number|string
  5. 化简:type A = number|string

如果条件类型没有分配性质,最终的结果将是never

6.5.2 infer关键字

条件类型的最后一个特性是可以在条件中声明泛型。在条件类型中声明泛型不使用这个句法,而使用infer关键字。

下面声明一个条件类型ElementTpe,获取数组中元素的类型:

// 获取数组中元素的类型: T[number]索引类型
type ElementType<T> = T extends unknown[] ? T[number] : T
type A = ElementType<(number|string)[]> // string|number
type B = ElementType<string> //string

使用infer关键字可以重写为:

type ElementType2<T> = T extends (infer U)[] ? U : T
type B = ElementType2<number[]> // number

这里,ElementType和ElementType2是等价的。注意,infer子句声明了一个新的类型变量U,Typescript将根据传给ElementType2的T推导U的类。

另外注意,U是在行内声明的,而没有和T一起在前声明。倘若在前面声明,结果如何?

type ElementUgly<T,U> = T extends U[] ? U : T;
type C = ElementUgly<number[]>// 需要两个参数

更加复杂的例子

type SecondArg<F> = 
F extends (a: any, b: infer B) => any ? B : never// 获取Array.slice的类型
type F = typeof Array['prototype']['slice']
type A = SecondArg<F> // number|undefined

可见,[].slice的第二个参数是number|undefined类型。而且在编译时便可知晓这一点。

6.5.3 内置的条件类型

利用条件类型可以在类型层面表达一些强大的操作,Typescript自带了一些全局可用的条件类型

Exclude<T,U>

和前面的Without类型,计算在T中而不在U中的类型:

type A = number|string
type B = string
type C = Exclude<A,B>// number

Extract<T,U>

计算T中可赋值给U的类型:

type A = number|string
type B = string
type C = Extract<A,B> // string

NonNullable

从T中排除null和undefined

type A = {a?:number|null}
type B = NonNullable<A['a']>// number

ReturnType

计算函数的返回类型(注意,不适用于泛型和重载的函数)

type F = (a:number) = string
type R = ReturnType<F> // string

InstanceType

计算类构造方法的实例类型

type A = {new():B}
type B = {b:number}
type I = InstanceType<A> // {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);

Exclude<T,U>

和前面的Without类型,计算在T中而不在U中的类型:

type A = number|string
type B = string
type C = Exclude<A,B>// number

6.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(<string>input)//2.
  1. 使用断言(as)告诉Typescript,input是字符串,而不是string|number类型。如果想先测试formatInput函数,而且肯定getUserInput函数返回一个字符串,就可以这么做
  2. 类型断言的旧句法使用<>尖括号。这两种句法是相同的作用。

优先使用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
}
  1. 如果没有id就返回
  2. 在下一次时间循环时,删除对话框
  3. 身处一个箭头函数中,开始一个新作用域,Typescript不知道1.和2.之间的代码修饰了dialog,因此1.中所做的类型细化不起作用。
  4. 类似的,尽管我们知道对话框一定在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 tuple<T extends unknown[] // 2.
>(...ts:T):T{return ts
}
interface Array<T> {//1.zip<U>(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(n=>n*2).zip(['a','b','c']));//[ [ 2, 'a' ], [ 4, 'b' ], [ 4, 'c' ] ]
  1. 首先让Typescript知道我们要为Array添加zip方法。我们利用接口合并特性(5.4.1节)增强全局接口Array,为这个全局定义的接口添加zip方法。这个文件没有显示导出或导入(意味着在脚本模式,10,2,3节),因此可以直接增强全局接口Array。我们声明一个接口,与现有的Array同名,Typescript负责将二者合并。如果文件在模块模式中(需要导入其他代码,便是这种情况),就要把全局扩展放在declare global类型声明中(11.1节),global是一个特殊的命名空间,包含所有全局定义的值(在模块模式中无需导入就能使用任何值,见第十章),可以增强模块模式文件中全局作用域内的名称。
  2. 然后在Array的原型上实现zip方法。这里使用this类型,以便让Typescript正确推导出调用zip方法的数组的类型T
  3. 由于Typescript推导出的映射函数的返回类型是(T|U)[](Typescript没那么智能,意识不到这个元组的0索引始终是T,1索引始终是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(n=>n*2).zip(['a','b','c']));

相关文章:

Typescript第六章 类型进阶(类型之间的关系,全面性检查,对象类型进阶,函数类型进阶,条件类型等)

文章目录 第六章 类型进阶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…...

kernel32.dll如何修复,快速解决kernel32.dll缺失的方法

Kernel32.dll是Windows操作系统中一个重要的系统文件&#xff0c;对于系统的正常运行至关重要。然而&#xff0c;由于各种原因&#xff0c;用户可能会遇到kernel32.dll文件的缺失问题。今天小编就来给大家详细的介绍一下kernel32.dll这个文件&#xff0c;并且详细的介绍一下ker…...

初始化前端项目配置 eslint、prettier、husky 等等

每次新项目都要重新配置一遍&#xff0c;有点麻烦&#xff0c;记录一下。 一、配置 ESLint 1.1 核心配置 执行 npm init eslint/config 命令进行初始化&#xff0c;根据提示一路下一步即可&#xff0c;完成后会自动生成 eslintrc 文件并安装相关依赖。 1.2 React 编译模式配…...

嵌入式存储器为AI的实现提供了实现架构

近年来&#xff0c;大脑启发式计算机领域的研究活动获得了巨大的发展。主要原因是试图超越传统的冯诺依曼架构的局限性&#xff0c;后者越来越受存储器-逻辑通信的带宽和等待时间的局限性的影响。在神经形态架构中&#xff0c;内存是分布式的&#xff0c;可以与逻辑共定位。鉴于…...

iOS开发-格式化时间显示刚刚几分钟前几小时前等

iOS开发-格式化时间显示刚刚几分钟前几小时前等 在开发中经常遇到从服务端获取的时间戳&#xff0c;需要转换显示刚刚、几分钟前、几小时前、几天前、年月日等格式。 主要用到了NSCalendar、NSDateComponents这两个类 NSString *result nil;NSCalendarUnit components (NSC…...

ffmpeg视频音频命令

视频音频合并 视频音频合并&#xff0c;以视频时间为主&#xff0c;音频短了循环 方法1&#xff1a;混音&#xff0c;视频权重0&#xff0c;volume调节音量&#xff0c;aloop无限循环&#xff0c;duration:first为第一个素材的长度 ffmpeg -i video.mp4 -i audio.mp3 -filter_…...

Jenkins工具系列 —— Jenkins 安装并启动

文章目录 安装涉及相关链接选择安装Jenkins版本安装JenkinsJenkins web页面启动卸载Jenkins 安装涉及相关链接 Jenkins官网&#xff1a; https://www.jenkins.io/zh/ Jenkins下载安装步骤&#xff1a; https://www.jenkins.io/zh/download/ 安装各种版本OpenJDK&#xff1a; h…...

使用中间人攻击的arp欺骗教程

文章目录 前言一、查看网络接口配置第 1 步&#xff1a;从受害者处获取 IP 配置第 2 步&#xff1a;在 Linux 中打开数据包转发第 3 步&#xff1a;使用 arpspoof 将包重定向到您的计算机步骤4&#xff1a;拦截来自路由器的包裹步骤5&#xff1a;从目标的浏览器历史记录中嗅探图…...

设计模式、Java8新特性实战 - List<T> 抽象统计组件

一、背景 在日常写代码的过程中&#xff0c;针对List集和&#xff0c;统计里面的某个属性&#xff0c;是经常的事情&#xff0c;针对List的某个属性的统计&#xff0c;我们目前大部分时候的代码都是这样写&#xff0c;每统计一个变量&#xff0c;就要定义一个值&#xff0c;且…...

【JavaEE初阶】博客系统后端

文章目录 一. 创建项目 引入依赖二. 设计数据库三. 编写数据库代码四. 创建实体类五. 封装数据库的增删查改六. 具体功能书写1. 博客列表页2. 博客详情页3. 博客登录页4. 检测登录状态5. 实现显示用户信息的功能6. 退出登录状态7. 发布博客 一. 创建项目 引入依赖 创建blog_sy…...

day51-Mybatis-Plus/代码生成器

1.Mybatis-Plus 定义&#xff1a;是一个Mybatis的增强工具&#xff0c;只在Mybatis基础上增强不做改变&#xff0c;简化开发&#xff0c;提升效率 2.MP实战 2.1 创建springboot工程&#xff0c;勾选web&#xff0c;引入依赖 <dependency> <groupId>mysql<…...

22.Netty源码之解码器

highlight: arduino-light 抽象解码类 https://mp.weixin.qq.com/s/526p5f9fgtZu7yYq5j7LiQ 解码器 Netty 常用解码器类型&#xff1a; ByteToMessageDecoder/ReplayingDecoder 将字节流解码为消息对象&#xff1b;MessageToMessageDecoder 将一种消息类型解码为另外一种消息类…...

R语言【Tidyverse、Tidymodel】的机器学习方法

机器学习已经成为继理论、实验和数值计算之后的科研“第四范式”&#xff0c;是发现新规律&#xff0c;总结和分析实验结果的利器。机器学习涉及的理论和方法繁多&#xff0c;编程相当复杂&#xff0c;一直是阻碍机器学习大范围应用的主要困难之一&#xff0c;由此诞生了Python…...

vscode 第一个文件夹在上一层文件夹同行,怎么处理

我的是这样的 打开终端特别麻烦 解决方法就是 打开vscode里边的首选项 进入设置 把Compact Folders下边对勾给勾掉...

[JavaScript游戏开发] 绘制冰宫宝藏地图、人物鼠标点击移动、障碍检测

系列文章目录 第一章 2D二维地图绘制、人物移动、障碍检测 第二章 跟随人物二维动态地图绘制、自动寻径、小地图显示(人物红点显示) 第三章 绘制冰宫宝藏地图、人物鼠标点击移动、障碍检测 第四章 绘制Q版地图、键盘上下左右地图场景切换 文章目录 系列文章目录前言一、本章节…...

【NLP概念源和流】 01-稀疏文档表示(第 1/20 部分)

一、介绍 自然语言处理(NLP)是计算方法的应用,不仅可以从文本中提取信息,还可以在其上对不同的应用程序进行建模。所有基于语言的文本都有系统的结构或规则,通常被称为形态学,例如“跳跃”的过去时总是“跳跃”。对于人类来说,这种形态学的理解是显而易见的。 在这篇介…...

服务器运行python程序的使用说明

服务器的使用与说明 文章目录 服务器的使用与说明1.登录2.Python的使用2.1 服务器已安装python32.2 往自己的用户目录安装python31.首先下载安装包2.解压缩3.编译与安装 2.3 新建环境变量2.4 测试 3 创建PBS作业并提交 1.登录 windowsr打开运行命令窗口&#xff0c;在运行框中…...

8.2一日总结

1.记录更新&#xff1a; untracked&#xff1a; 未追踪&#xff08;新增的文件&#xff09; unmodefied&#xff1a; 未修改 modefied&#xff1a; 已修改 staged&#xff1a; 已暂存 2、添加指定文件到暂存区&#xff1a; git add 文件名 gi…...

JavaScript(四)DOM及CSS操作

1、DOM简介 DocumentType: Html的声明标签 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Docume…...

window中,关闭java占用端口的进程

查看端口被占用的情况 netstat -ano|findstr "端口号"使用Tasklist查看对于 PID 的进程名 tasklist|findstr "PID号"通过 taskkill 命令方式结束进程 taskkill /f /t /im Pid...

浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)

✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义&#xff08;Task Definition&…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战

前言 现在我们有个如下的需求&#xff0c;设计一个邮件发奖的小系统&#xff0c; 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其…...

PHP和Node.js哪个更爽?

先说结论&#xff0c;rust完胜。 php&#xff1a;laravel&#xff0c;swoole&#xff0c;webman&#xff0c;最开始在苏宁的时候写了几年php&#xff0c;当时觉得php真的是世界上最好的语言&#xff0c;因为当初活在舒适圈里&#xff0c;不愿意跳出来&#xff0c;就好比当初活在…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器

——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的​​一体化测试平台​​&#xff0c;覆盖应用全生命周期测试需求&#xff0c;主要提供五大核心能力&#xff1a; ​​测试类型​​​​检测目标​​​​关键指标​​功能体验基…...

如何在看板中有效管理突发紧急任务

在看板中有效管理突发紧急任务需要&#xff1a;设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP&#xff08;Work-in-Progress&#xff09;弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中&#xff0c;设立专门的紧急任务通道尤为重要&#xff0c;这能…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)

参考官方文档&#xff1a;https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java&#xff08;供 Kotlin 使用&#xff09; 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...

Element Plus 表单(el-form)中关于正整数输入的校验规则

目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入&#xff08;联动&#xff09;2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...

大数据学习(132)-HIve数据分析

​​​​&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4…...

AspectJ 在 Android 中的完整使用指南

一、环境配置&#xff08;Gradle 7.0 适配&#xff09; 1. 项目级 build.gradle // 注意&#xff1a;沪江插件已停更&#xff0c;推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...