「✍ Typescript」类型

318 阅读6分钟

keyof 和 In

keyof: 将interface/type转为联合类型

in: 作用于联合类型, 用于枚举联合类型的值(注:不可用于interface中!)

const person = {
    name: 'aa',
    age: 1
}
interface IPerson {
    name: string
    age: number
}

type UnionP = keyof typeof person // 'name' | 'age'
type UnionP1 = keyof IPerson // 'name' | 'age'

type P = {
    [p in UnionP1]: string
} // { name: string, age: string}

type p1 = {
    [p in UnionP1]: string
} // { name: string, age: string}
interface T0{
  name: string
  age: number
}

keyof T0 用于获取key组成的联合类型 => 'name' | 'age'

T0[keyof T0] 用于获取value组成的联合类型 => string | number

|&的区别

经过实验

IProps1 & IProps2 - 交叉类型 : 取并集

IProps1 | IProps2 - 联合类型 = Partical<IProps1 & IProps2>

IProps1:{
  name: string
  age: number
}
IProps2:{
  school: string
}

IProps1 & IProps2类型的变量必须同时包含name、age、school

IProps1 | IProps2只要满足IProps1或IProp2其中一个即可,可以多不能少,如 { school:string, name: string} 是可以通过的,因为满足了IProps2

实现真正的「或」逻辑

我们会发现,编写类型时使用的 | 并不是我们理解的

interface IProps1 {
  firstName: string
  lastName: string
}
interface IProps2 {
  fullName: string
}

type IPerson = IProps1 | IProps2

期望:ts报错
实际:通过ts校验
const p1 = {
  firstName: 'jenson',
  lastName: 'liu',
  fullName: 'jensonliu'
}

我也不知道这是设计失误还是设计本意,如果想模拟出真正的逻辑,还是需要一点黑科技的。

思路

使用never类型

interface IProps1 {
  firstName: string
  lastName: string
  fullName?: never
}
interface IProps2 {
  firstName?: never
  lastName?: never
  fullName: string
}

type IPerson = IProps1 | IProps2

这就是手动告诉编译器,fullName存在时firstNamelastName都为never,反之亦然

改进

当然,我们不能对每个interface都去手动写never,因此可以抽取成通用的类型方法

type ExcludeToNever<T, V> = {
  [key in keyof Omit<V, keyof T>]?: never; // V中存在而T中不存在的属性
} & {
  [key in keyof T]: T[key]; // T的原有属性
};

ExcludeToNever传入两个interface,即T和V, 将V中存在而T中不存在的属性设置为never并塞入T中

然后

type IOr<F, S> = ExcludeToNever<F, S> | ExcludeToNever<S, F>

这样一来就将两个interface中彼此不存在的key都用never填充了

extends

用于继承

interface IProps extends IBaseProps { }

用于三目运算

type IProps<T,U> T extends U ? X : Y

当T、U为interface、type时,extends单纯用于判断继承关系

但当T、U为联合类型时,该运算会遵循类似分解因式的规范

a(b+c) = ac + bc

T = 'a' | 'b'
T extends U  => 'a' extends U | 'b' extends U

利用这个性质可以完成一些工具类型

T = 'a' | 'b'
U = 'b' | 'c' | 'd'

// 求交集
Intersection<T,U> = T extends U ? T : never 

Intersection<'a'|'b', 'b'|'c'|'d'>
 = 'a'|'b' extends 'b'|'c'|'d'
 = 【'(a' extends 'b'|'c'|'d') ? 'a' : never】 | 【('b' extends 'b'|'c'|'d') ? 'b' : 'never'】
 = 'b'
 
------------------------------------------------------------------

// 求差集
Diff<T, U> = T extends U ? never : T 
原理同上

infer

infer是被用在extends中, 用于推导泛型的关键字。

我的理解中,就是将泛型声明为变量并使用的东西,举个例子

我们希望有一个工具类型,ArrChildType<T>来推导数组元素的类型,我们可以这么写

type ArrChildType<T> = T extends Array<P> ? P : T // error Exported type alias 'ArrChildType' has or is using private name 'P'.
type result = ArrChildType<Array<number>>

但会发现报红了,泛型P不能这样使用,于是infer就派上用场了

type ArrChildType<T> = T extends Array<infer P> ? P : T // success
type result = ArrChildType<Array<number>> // number

Parameters

用于获取函数的入参类型

注:Parameters返回结果为tuple数组类型

declare function f1(arg: { a: number; b: string }): void;
 
type T0 = Parameters<() => string>; // []
     
type T1 = Parameters<(s: string) => void>; // [s: string]
     
type T2 = Parameters<<T>(arg: T) => T>; // [arg: unknown]
     
type T3 = Parameters<typeof f1>; // [arg: {a: number, b: string}]
     
type T4 = Parameters<any>; // unknown[]
     
type T5 = Parameters<never>; // never
     

ReturnType

用于获取函数返回值类型

declare function f1(): { a: number; b: string };
 
type T0 = ReturnType<() => string>; // string
     
type T1 = ReturnType<(s: string) => void>; // void

type T2 = ReturnType<<T>() => T>; // unknown
     
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
     
type T4 = ReturnType<typeof f1>; // {a: number, b: string}

type T5 = ReturnType<any>; // any
     
type T6 = ReturnType<never>; // never

as const

as const 被称为 const 类型断言,const 类型断言告诉编译器,要将这个表达式推断为最具体的类型,如果不使用它的话,编译器会使用默认的类型推断行为,可能会把表达式推断为更通用的类型。

举个例子, 有这样一个数据结构

const arr = ]
  {
    name: 'a1',
    type: 'type1'
  },
  {
    name: 'a2',
    type: 'type2'
  },
  {
    name: 'a3',
    type: 'type3'
  }
] as const

我们想把所有的type抽出来做一个联合类型 IType = 'type1' | 'type2' | 'type3', 我们会写成这样

type IType = typeof arr[number]['type'] 

其中arr[number]是指取arr中的item类型

我们会发现如果没有加as const,会得到string,也就是上面所说的,被推断成了更通用的类型,编译器认为你的type是可能变动的。

当加上了as const,结果是'type1' | 'type2' | 'type3'

下面这个例子也是同理

const Status = {
  suc: 0,
  fail: -1,
  pending: 1
} as const
 

type IStatus = keyof typeof Status // 'suc' | 'fail' | 'pending'

type IStatusCode = typeof Status[keyof typeof Status] // 0 | -1 | 1 若没有as const则为number

替换对象中的key对象类型

思路:先用Omit剔除传入的key枚举,然后 & 上传入的新对象类型

单属性替换

/**
 *  替换对象中的属性
 *  e.g type T= ReplaceKey<typeof obj, 'name'., string>
 */
export type ReplaceKey<O, K extends keyof O, T> = Omit<O, K> & { [p in K]: T }

// K extends keyof O 约束传入的key类型必须在obj内

批量替换


/**
 *  批量替换对象中的属性
 */
export type ReplaceKeys<O, K> 
    = Omit<O, keyof K> & { [p in keyof K]: K[p] } // p in typeof K枚举key值,K[p]取当前key的类型值


const obj = {
   key1: 1,
   key2: 2
}
type T= ReplaceKeys<typeof obj, { key1: string, key2: string }> 
// or
type T= ReplaceKeys<typeof obj, { [p in keyof typeof obj]: string }> 
// => { key1: string, key2: string}

保留/剔除对象类型中指定类型值


type T0 = {
  name: string
  age: never
}

// 曲线救国,如果value!==Condition, 就把value改为和key一样用于最后[keyof T]取值
type GetKeyByValueType<T, Condition> = Pick<
  T,
  {
    [key in keyof T]: T[key] extends Condition ? key : never
  }[keyof T]
>

// 用Omit来取反
type excludeKeyByValueType<T, Condition> = Omit<
  T,
  {
    [key in keyof T]: T[key] extends Condition ? key : never
  }[keyof T]
>

// 获取为never的
type T = GetKeyByValueType<T0, never>  // { age: never }

// 剔除为never的
type T2 = excludeKeyByValueType<T0, never> // { name: string }

is

is 通常用于做类型保护,常用于判断类型的工具Function中

假设有一个判断是否为字符串数组的方法

const isStringArr = (params: unknown) : boolean => {
    if(!(params instanceof Array)){
        return false
    }
    return params?.every(item=>typeof item === 'string')
} 

const s1 = ''

const result = isStringArr(s1) ? s1.join(',') : '' // error

会发现s1.join除爆红,typescript认为s1还是定义时的'',我们用is来保护该方法的返回值


const isStringArr = (params: unknown) : params is string[] => {
    if(!(params instanceof Array)){
        return false
    }
    return params?.every(item=>typeof item === 'string')
} 

const s1 = ''

const result = isStringArr(s1) ? s1.join(',') : '' // ok

总结:

  • 在使用类型保护时,TS 会进一步缩小变量的类型。例子中,将类型从 any 缩小至了 string[]
  • 类型保护的作用域仅仅在 if/三元表达等判断语句后的块级作用域中生效

求交集

前置API: Extract

type Extract<T, U> = T extends U ? T : never;

联合类型的交集

type A = 'a' | 'b' | 'c'
type B = 'a' | 'd'

type UK = Extract<A, B> // 'a'

原理:联合类型做extends时会类似分配律的形式进行

a | b | c extends a | d = [a extends a | d] | [b extends a | d]

最终结果会将never过滤掉,因此得到的是交集

interface的交集

type IO1 = {
    name: string
    age: number
    school: string
}

type IO2 = {
    school: string
    sex: number
}
type UK = Extract<keyof IO1, keyof IO2>

原理:转为联合类型再用Extract

剔除interface中value为某种类型的项

type PickWithoutConToUnion<T, Con> = { [key in keyof T]: Con extends T[key] ? never : key }[keyof T]

type PickWithCon<T, K> = Pick<T, PickWithoutConToUnion<T,K>>

type Result = PickWithCon<IO1,string>

原理:PickWithoutConToUnion 先将interface中的符合过滤条件的value全部转成never,不符合过滤条件的转为key值,然后把value转为联合类型,联合类型会过滤掉never,剩下的就是没有被过滤掉的key值联合类型了,最后再Pick一下