一文带你进阶typeScript高级用法
前言
学习并且用了一段时间typeSript了,总感觉有些概念理解的不够透彻,有点似懂非懂,知识点,概念较为分散。今天通过一篇文章的总结,把ts中的高级知识点进行总结梳理一遍。
交叉类型 &
相当于js中的Object.assign(),将两个对象的属性进行合并,
type ObjType = {a:string} & {b:number}
type Test = {a:string,b:number} extends ObjType ? true : false // true
const merge = <T, U>(obj1: T,obj2: U):T & U => {
return Object.assign(obj1, obj2) as T & U
}
merge({a:1},{b:2}) // {a:1,b:2}
交叉类型会将同一个类型合并,不同类型舍弃
type MyNerver = string & number // string和number进行合并,会返回一个never
联合类型 |
表示一个变量可以是A类型也可以是B类型
如下表示数组的元素既可以是string类型也可以是number类型
const arr:(string | number)[] = [1,'aa']
// 使用联合类型定义方位
type Direction = 'north' | 'east' | 'south' | 'west'
function getDirectionFirstLetter(direction: Direction): string{
return direction.substr(0,1)
}
当类型参数传入的是一个联合类型,ts会将传入的联合类型进行遍历运算,返回一个计算后的联合类型
type MarginType<T> = `margin-${T}`
type Ditection = 'top' | 'right' | 'boottom' | 'left'
type TestMarginType = MarginType<Ditection> // 'margin-top' | 'mamrgin-right' | 'mamrgin-boottom' | 'margin-left'
类型守卫
有时候一个参数或者一个对象,可能是两种不同类型的数据,这个时候Ts无法识别出来变量具体对应哪一个类型,在使用的时候无法进行断言提醒,会进行报错提示,这个时候就需要使用类型守卫进行范围限定,可以使用typeof instanceof in is四种方法进行类型守卫
in(属性判断)
interface Foo {
foo: string
}
interface Bar {
bar: string
}
// 错误
function test(obj: Foo | Bar){
console.log(obj.foo) // Property 'foo' does not exist on type 'Bar'
console.log(obj.bar) // Property 'bar' does not exist on type 'Foo'
}
// 正确
function test(obj: Foo | Bar){
if('foo' in Foo){ // 使用in约束范围
console.log(obj.foo)
}else{
console.log(obj.bar)
}
}
instanceof
class Bird{
fly(){}
eat(){}
}
class Fish{
swim(){}
eat(){}
}
function getRandomAnimal(): Fish | Bird{
return Math.random() > .5 ? new Bird() : new Fish()
}
const animal = getRandomAnimal()
if(animal instanceof Bird){
animal.fly()
aimal.swim() // Property 'swim' does not exist on type 'Bird'
}else{
animal.swim()
}
typeof
// 当不知道参数的具体类型的时候,对参数进行操作会报错
function getLength(x: string | number){
console.log(x.length) // Property 'length' does not exist on type 'number'.
console.log(x.toFixed(2)) // Property 'toFixed' does not exist on type 'string'
}
getLength('hello')
// 需要使用typeof进行类型守卫
function getLength(x: string | number){
if(typeof x === 'string'){
console.log(x.length) // ts推断出x为string类型
console.log(x.toFixed(2)) // Property 'toFixed' does not exist on type 'string'
}else{
console.log(x.toFixed(2)) // ts推断出x为number类型
}
}
getLength('hello')
is
const isNumber = (x: unknown): boolean => typeof x === 'number' // 这种写法不具有类型守卫的能力
// is的使用
const isNumber = (x: unknown): x is number => typeof x === 'number'
const isString = (x: unknown) : x is string => typeof x === 'string'
function padLeft(val: string, padding:string | number){
if(isNumber(padding)){
return Array(padding).join(' ') + val
}else{
return padding + val
}
}
// 此处的x is number, x is string是关键,只有通过is指定函数的返回类型,函数才拥有类型守卫的能力
! 的使用
! 用来进行非空断言,使用! 可以断言对象是非null和undefined类型
function toLowerCase(str: string | null | undefined){
//return str.toLowerCase() // 报错,'str' is possibly 'null' or 'undefined'
return str!.toLowerCase()
}
枚举的使用
使用枚举可以增加代码的语义化,例如后端返回status参数0,1,2分别代表商品的删除,上架,下架,为了让0,1,2更具有语义化,可以定一个枚举类型
enum Status {
DELETE = 0,
ONLINE = 1,
OFFLINE = 2,
}
function getShopStatus(status: number): string{
if(status === Status.ONLINE) return '上架'
if(status === Status.OFFLINE) return '下架'
if(status === Status.DELETE) return '删除'
return 'error'
}
装饰器的使用
在不改变类的内部结构下,对类进行修饰
不使用装饰器,对一个类的方法添加try catch
const userInfo:unknown = undefined
interface UserInfo {
name: string
age: number
}
class User{
name:string;
age:number;
constructor(userInfo:UserInfo){
this.name = userInfo.name
this.age = userInfo.age
}
getName(){
try{
return userInfo.name // 此处userInfo.name 会报错,因为前面定义了一个全局变量
}catch{
console.log('userInfo.name不存在')
}
}
getAge(){
try{
return userInfo.age // 此处userInfo.age 会报错,因为前面定义了一个全局变量
}catch(e){
console.log('userInfo.age不存在')
}
}
}
const zhangsan = new User({name:'zhangsan',age:28})
写一个try catch的装饰器进行改装
const userInfo:any = undefined
interface UserInfo{
name: string
age: number
}
function tryCatchDecorator(errorMsg: string){
return function(target:any, key: string, descriptor: any){
const originFn = descriptor.value
descriptor.value = ()=>{
try{
originFn()
}catch(e){
console.log('error' + errorMsg)
}
}
}
}
class User{
name:string;
age:number;
constructor(userInfo:UserInfo){
this.name = userInfo.name
this.age = userInfo.age
}
@tryCatchDecorator('nameError')
getName(){
return userInfo.name
}
@tryCatchDecorator('ageError')
getAge(){
return userInfo.age
}
}
const zhangsan = new User({name:'zhangsan',age:28})
zhangsan.getName()
zhangsan.getAge()
装饰器的使用总结:
-
装饰器可以叠加使用,调用的顺序为从下往上
-
装饰器需要传参,则将装饰器返回一个函数
-
类装饰器传入的参数,为类的构造函数
-
类成员方法传入的参数,分别为target: 对应类的prototype,key: 函数的名称 descriptor: 属性的[描述器]
function tryCatchDecorator(errorMsg:string){ return function(target:any, key: string, descriptor: any){ console.log('target',target) console.log('key',key) // getAge console.log('descriptor',descriptor) /* { value: [Function: getAge], writable: true, enumerable: false, configurable: true } */ const originFn = descriptor.value descriptor.value = ()=>{ try{ originFn() }catch(e){ console.log('error'+errorMsg) } } } }
extends关键字
extends有两种用法
1.表示条件判断
type IsTwo<T> = T extends 2 ? true : false
2.限定范围,类型约束
type ConcatStr<S1 extends string,S2 extends string> = `${S1}-${S2}`
infer关键字
声明一个类型变量,用来存储在模式匹配中所捕获的数据,相当于一个占位符,使用这个占位符去提取一个表达式中的数据
例如: 获取函数的返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
获取函数的参数
type GetParams<T> = T extends (infer P) => any ? P : never
keyof关键字
相当于js中的Object.keys()获取一个对象的属性值
interface Person {
name: string
age: number
}
function getPersonValue(p: Person, k: keyof Person){
return p[k]
}
// keyof Person相当于 type PersonPops = 'name' | 'age'
如:定义一个根据对象属性获取对象值的方法
function getValue<T,K extends keyof T>(obj: T, key: K): T[K]{
return obj[key]
}
const obj = {
a: 111,
b: 222,
c: 333
}
getValue(obj,'a') // 111
getValue(obj,'d') // 报错,对象obj中不存在属性d
in 关键字
in用来实现映射类型,相当于for in循环,用来遍历一个对象的属性,如下: 将一个对象的属性都变为只读属性
type ReadOnly<T> = {
readonly [P in keyof T]: T[P]
}
interface Person{
name: string
age: number
sex: string
}
const person:ReadOnly<Person>{
name: '刘德华',
age: 48,
sex: '男'
}
person.name = '成龙' // 报错,只读属性不能进行修改赋值
typeof关键字
将一个普通的js对象转化为ts类型
const person = {
name: '刘的华',
age: 48,
sex:'男'
}
type Person = typeof person
/*
typepf person
相当于
{
name: string
age: number
sex: string
}
*/
function add(a: number, b: number): number{
return a + b
}
type AddType = typeof add // (a:number,b:number) => number
const断言的对象,使用typeof转化的时候会变成readonly只读属性
const person = {
name: '刘德华',
age: 48,
sex: '男'
} as const
type Persontype = typeof person
/*
结果是
{
readonly name: string
readonly age: number
readonly sex: string
}
*/
as const关键字
指定一个对象为只读属性,使用了as const对变量断言之后,在进行修改会报错
const foo = ['As const means immutable',123] as const
foo.push(222) // Property 'push' does not exist on type 'readonly
foo[1] = 456 // Cannot assign to '1' because it is a read-only property
模板字面量
// 定义css padding rule
type CssPadding = 'padding-left' | 'padding-right' | 'padding-top' | 'padding-bottom'
// 使用模板字面量改造
type Direction = 'left' | 'right' | 'top' | 'bottom'
type CssPaddding = `padding-${Direction}` // 'padding-left' | 'padding-right' | 'padding-top' | 'padding-bottom'
type MarginPadding = `margin-${Direction}` // 'margin-left' | 'margin-right' | 'margin-top' | 'margin-bottom'
// 将两个字符串拼接
type ConcatStr<S1 extends string,S2 extends string> = `${S1}-${S2}`
type TestConcat = ConcatStr<'Hello','typeScript'> // Hello-typeScript
type AddPrefix<T extends string,P extends string> = `${P}/${T}`
type TextAddPrefix = AddPrefix<'getProductList','www.bff.com'> // www.bff.com/getProductList
//如果是传递一个联合类型呢?
type TestConcat = ConcatStr<'hello' | 'welcome','word' | 'today'>
// hello-word | hello-today | welcome-word | welcome-today
type InferRoot<T extends string> = T extends `${infer R}${Capitalize<Direction>}` ? R : T
type TestInferRoot = InferRoot<marginLeft> // margin
as关键字
使用as关键字结合in遍历,可以修改对象的key值
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]:() => T[K]
}
interface Person {
name: string
age: number
sex: string
}
type TestGetters = Getters<Person>
结果为
/*
{
getName: () => string
getAge: () => number
getSex: () => string
}
*/
any 与unknown
可以把任何变量定义为any类型,any类型的变量可以进行任何操作,不会进行类型检查或者类型断言,不能在编译时发现错误
也可以把任何类型的变量定义为unknown类型,但必须进行类型守卫,类型断言才能对变量进行操作,可以在编译时发现错误
因此我们应该尽量避免使用any,而使用unknown进行替换
// 以下代码在编译时不会报错,但在运行时会报错
function invokeCallback(callBack: any){
try{
callback()
}catch(err){
console.error(err)
}
}
invokeCallback(1) // TypeError: callBack is not a function
// 使用unknown进行替换
function invokeCallback(callBack: unknown){
try{
callback() // 'callBack' is of type 'unknown'.
}catch(err){
console.error(err)
}
}
invokeCallback(1)
// 将any替换成unknown之后编辑器报错了,需要借助in typeof instanceof as进行类型断言和守卫解决报错
function invokeCallback(callBack: unknown){
try{
if(typeof callBack === 'function'){ // 加上类型守卫,则不会报错
callback()
}
}catch(err){
console.error(err)
}
}
invokeCallback(1)
函数重载
我们知道很多库中都大量使用了函数重载
例如vue3中关于ref的重载
export function ref<T extends object>(
value: T
): [T] extends [Ref] ? T : Ref<UnwrapRef<T>>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value, false)
}
vueUse中的useLocalStorage
export function useLocalStorage(key: string, initialValue: MaybeComputedRef<string>, options?: UseStorageOptions<string>): RemovableRef<string>
export function useLocalStorage(key: string, initialValue: MaybeComputedRef<boolean>, options?: UseStorageOptions<boolean>): RemovableRef<boolean>
export function useLocalStorage(key: string, initialValue: MaybeComputedRef<number>, options?: UseStorageOptions<number>): RemovableRef<number>
export function useLocalStorage<T>(key: string, initialValue: MaybeComputedRef<T>, options?: UseStorageOptions<T>): RemovableRef<T>
export function useLocalStorage<T = unknown>(key: string, initialValue: MaybeComputedRef<null>, options?: UseStorageOptions<T>): RemovableRef<T>
为什么要使用函数重载,函数重载能够解决什么问题,有什么好处呢?
通常一个方法由于参数可以是多种类型,参数不同函数要做不同的处理,返回不同的结果。如果全都一次性定义的话,会导致代码不清晰。
而重载就是为了解决这个问题的,通过定义不同重载的方法,让函数的职责看起来遵循单一职责原理,同时使用了重载定义好了函数的类型,会拥有更好的类型检测与断言提醒
如下没有使用函数重载
function add(x:string | number,y: string | number): string | number{
if(typeof x === 'string' && typeof y === 'string'){
return `${x},${y} `
}else if(typeof x === 'string' && typeof y === 'number' || typeof x === 'number' && typeof y === 'string'){
return `${x},${y} `
}else{
return x + y
}
}
可以发现没有使用重载的函数,参数的类型定义和返回都混到一起了,不是代码看起来不是很清晰
使用函数重载进行改造
函数重载可以定义多个重载签名,重载签名定义函数的参数返回类型,返回类型,没有函数体的具体实现
函数重载需要定义个实现签名,实现签名不需要定义参数类型和返回类型,只需要实现具体函数即可
function add(x: string, y: string): string
function add(x: number, y: number): number
function add(x: string, y: number): string
function add(x: number, y: string): string
function add(x:unknown,y: unknown){
if(typeof x === 'string' && typeof y === 'string'){
return `${x},${y} `
}else if(typeof x === 'string' && typeof y === 'number' || typeof x === 'number' && typeof y === 'string'){
return `${x},${y} `
}else{
return x + y
}
}
经过函数重载进行改造之后,函数的功能一目了然
内置高级类型实现
Partial
转化为可选属性
type Mypartial<T> = {
[P in keyof T]?: T[P]
}
Required
转化为必选属性
type MyRequired<T> = {
[P in keyof T]-?:T[P]
}
Readonly
转化为可读属性
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
Pick
读取指定属性的值
type MyPick<T,K extends keyof T> = {
[P in K]: T[P]
}
Exclude
取差集,排除
type MyExclude<T,U> = T extends U ? never : T
type test = MyExclude<string | number | boolean, string | number> // boolean
Extract
取并集,提取
type MyExtact<T,U> = T extends U ? T : never
type test = MyExtact<string | number | boolean, string | number> // string | number
Omit
读取与指定属性相反的属性的值
type MyOmit<T,K extends keyof any> = Pick<T, Exclude<keyof T,K>>
Record
根据指定的key和value生成一个新的类型
type MyRecord<K extends keyof any, T> = {
[P in K]: T
}
type Test = MyRecord<'a'|'b'|'c', number>
const testRecord:Test = {
a: 1,
b: 2,
c: 3
}
Parameters
获取函数参数的类型
type MyParameters<T extends (...args:any) => any> = T extends (...args: infer P) => any ? P : never
function add(a: number, b: number): number{
return a + b
}
type Test = MyParameters<typeof add> // [a: number, b: number]
ReturnType
获取函数返回的值的类型
type MyReturnType<T extends (...args: any) => any> = T extends (...args: any)=> infer R ? R : any
function add(a: number, b: number): number{
return a + b
}
type Test = MyReturnType<typeof add> // number
总结
本文主要整理了ts中高级知识点的使用,如交叉类型,联合类型,extends关键字,infer关键字,keyof关键字,in关键字,类型安全守卫,装饰器的使用,函数重载的概念,内置高级类型的实现等。相信理解了文中的知识,你对typeScript的掌握一定会上一个新的台阶,加油打工人,今天是开工的第一天,让我们卷起来。