类型体操:实现RequiredByKeys<T, K>类型

199 阅读2分钟

题目

实现一个通用的RequiredByKeys<T, K>,它接收两个类型参数TK

K指定应设为必选的T的属性集。当没有提供K时,它就和普通的Required<T>一样使所有的属性成为必选的。

例如:

interface User {
  name?: string
  age?: number
  address?: string
}

type UserRequiredName = RequiredByKeys<User, 'name'> // { name: string; age?: number; address?: string }

解答

我们先来理解一下Required<T>类型,定义如下

type Required<T> = {
    [P in keyof T]-?: T[P]
};

使用了TypeScript内部的「映射类型」语法, { [P in K]: T }P in K 表达式类型于JavaScript for...in语法,用于遍历type中的所有类型K,以及类型变量T,用于表示TypeScript中的任何类型。

还可以在映射过程中使用附加的修饰符只读和问号 (?)。通过添加加号(+)和减号(-)前缀来添加和删除相应的修饰符。如果没有添加前缀,则默认使用加号。

使用场景为,把所有的可选类型,转为必要类型

type User = {
    name?:string;
    password?:string;
    address?:string;
    phone?:string;
}

type RequiredUser = Required<User>

实现思路是我们只需要选择与K关联的属性,根据需要设置并生成新的对象类型,然后根据剩余的属性构建另一个对象类型,最后使用&算子转换以上两种对象类型组合成一个新的对象类型

interface User {
  name?: string
  age?: number
  address?: string
}

type Merge<T> = {
    [P in keyof T]: T[P]
}

type RequiredByKeys<T, K = keyof T> = Merge<
{
    [P in keyof T as P extends K ? P : never]-?: T[P]
} & {
    [P in keyof T as P extends K ? never : P]: T[P]
}
>

type UserRequiredName = RequiredByKeys<User, 'name'>
// { name: string; age?: number; address?: string }

使用as子句语法重新映射类型中的健

type MappedTypeWithNewKeys<T> = {
    [K in keyof T as NewKeyType]:T[K]
}

其中NewKeyType的类型必须是string | number | symbol联合类型的子类型。在重新映射键的过程中,我们可以通过返回 never 类型来过滤键。

// 具体到 RequiredByKeys<User, 'name'>, 过滤得到{ name: string}
// never会过滤类型key
{
    [ P in keyof User as P extends "name" ? P : never]-?: T[P]
}
// 具体到 RequiredByKeys<User, 'name'>, 过滤得到{ age?:number; address?:string }
{
    [P in keyof User as P extends "name" ? never : P]: T[P]
}

甚至更简洁的解决方案如:

type RequiredByKeys<T, K = keyof T> = Merge<T & {
    [ P in keyof T as P extends K ? P : never]-?: T[P]
}>

type UserRequiredName = RequiredByKeys<User, 'name'>
// type UserRequiredName = {
//       name: string;
//       age?: number | undefined;
//       address?: string | undefined;
// }

参考