typescript类型体操练习

74 阅读12分钟

实现pick

不使用 Pick<T, K> ,实现 TS 内置的 Pick<T, K> 的功能

从类型 T 中选出符合 K 的属性,构造一个新的类型

/**
 * 思路
 * in关键字有两个作用
 * ① 可以作为类型守卫,用于判断某个属性是否存在于某个对象中
 * ② 以用于迭代对象的属性
 */
type MyPick<T,K extends keyof T>={
    [k in K]:T[k]
}
interface Todo {
    title: string
    description: string
    completed: boolean
}
​
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
​
const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}
​

对象属性只读

不要使用内置的Readonly<T>,自己实现一个。

泛型 Readonly<T> 会接收一个 泛型参数,并返回一个完全一样的类型,只是所有属性都会是只读 (readonly) 的。

也就是不可以再对该对象的属性赋值。

/**
 * 思路
 * readonly 修饰对象属性
 * in关键字 迭代对象的属性
 */
type MyReadonly<T> = {
    readonly [K in keyof T]: T[K]
}
​
interface Todo {
  title: string
  description: string
}
​
const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}
​
todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property

元组转换为对象

将一个元组类型转换为对象类型,这个对象类型的键/值和元组中的元素对应。

/**
 * 思路
 * 元组转对象,extends类型收窄,限制只能是string|number|symbol类型的数组
 * 数组取值通过T[number]的方式
 * in操作符循环属性
 */
type TupleToObject<T extends readonly (string|number|symbol)[]> = {
    [key in T[number]]: key
}
​
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
​
type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

第一个元素

实现一个First<T>泛型,它接受一个数组T并返回它的第一个元素的类型。

/**
 * 思路
 * 如果数组存在就返回数组第一项T[0]
 * T extends [infer A, ...infer reset] ? A : never; infer解构数组第一项并返回
 */
// type First<T extends any[]> = T extends any[] ? T[0] : never; 第一种解决办法
// type First<T extends any[]> = T['length'] extends 0 ? never : T[0]; 第二种解决办法
type First<T extends any[]> = T extends [infer A, ...infer reset] ? A : never; //第三种解决办法
​
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
​
type head1 = First<arr1> // 应推导出 'a'
type head2 = First<arr2> // 应推导出 3

获取元组长度

创建一个Length泛型,这个泛型接受一个只读的元组,返回这个元组的长度。

/**
 * 思路
 * 获取数组的长度可以使用T['length']
 * T extends { length: infer L } 或者把数组的长度属性赋值给变量L,并返回L
 */
type Length<T extends readonly any[]> = T['length'] // 方法一
// type Length<T extends readonly any[]> = T extends { length: infer L } ? L : never; // 方法二
​
type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']
​
type teslaLength = Length<tesla> // expected 4
type spaceXLength = Length<spaceX> // expected 5

实现Exclude

实现内置的 Exclude<T, U> 类型,但不能直接使用它本身。

从联合类型 T 中排除 U 中的类型,来构造一个新的类型。

/**
 * 思路
 * 判断U是否是T的类型或它的子集
 * 会被解析为 (a extends a ? never : a) | (b extends a ? never : b) | (c extends a ? never : c)
 */
type MyExclude<T, U> = T extends U ? never : T
​
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

Awaited

假如我们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,我们用 Promise 中的 T 来描述这个 Promise 返回的类型。请你实现一个类型,可以获取这个类型。

/**
 * 思路
 * T extends PromiseLike<infer U>判断T是否是PromiseLike<infer U>类型或它的子集
 * 把Promise返回值类型赋值给变量infer U,返回Promise返回值变量给MyAwaited
 */
type MyAwaited<T> = T extends PromiseLike<infer U> ? MyAwaited<U> : T;
​
type ExampleType = Promise<string>
​
type Result = MyAwaited<ExampleType> // string

If

实现一个 IF 类型,它接收一个条件类型 C ,一个判断为真时的返回类型 T ,以及一个判断为假时的返回类型 FC 只能是 true 或者 falseTF 可以是任意类型。

type If<C extends boolean, T, F> = C extends true ? T : Ftype A = If<true, 'a', 'b'>  // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'

Concat

在类型系统里实现 JavaScript 内置的 Array.concat 方法,这个类型接受两个参数,返回的新数组类型应该按照输入参数从左到右的顺序合并为一个新的数组。

type Concat<T extends readonly unknown[], U extends readonly unknown[]> = [...T, ...U]
​
type Result = Concat<[1], [2]> // expected to be [1, 2]

Includes

在类型系统里实现 JavaScript 的 Array.includes 方法,这个类型接受两个参数,返回的类型要么是 true 要么是 false

type Includes<T extends readonly any[], U> = {
    [P in T[number]]: true
}[U] extends true ? true : false;
​
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`

Push

在类型系统里实现通用的 Array.push

type Push<T extends readonly unknown[], U> = [...T, U]
​
type Result = Push<[1, 2], '3'> // [1, 2, '3']

Unshift

实现类型版本的 Array.unshift

type Unshift<T extends readonly unknown[], U> = [U, ...T]
​
type Result = Unshift<[1, 2], 0> // [0, 1, 2]

Parameters

实现内置的 Parameters 类型

type MyParameters<T extends (...arg: any[]) => unknown> = T extends (...arg: infer S) => unknown ? S : unknown
​
const foo = (arg1: string, arg2: number): void => {}
​
type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]

ReturnType

不使用 ReturnType 实现 TypeScript 的 ReturnType<T> 泛型。

type MyReturnType<T extends (...arg: any[]) => unknown> = T extends (...arg: any[]) => infer A ? A : unknown
​
const fn = (v: boolean) => {
  if (v)
    return 1
  else
    return 2
}
​
type a = MyReturnType<typeof fn> // 应推导出 "1 | 2"

Omit

不使用 Omit 实现 TypeScript 的 Omit<T, K> 泛型。

Omit 会创建一个省略 K 中字段的 T 对象。

type MyOmit<T, K extends keyof T> = {[P in keyof T as P extends K ? never: P] :T[P]}
​
interface Todo {
  title: string
  description: string
  completed: boolean
}
​
type TodoPreview = MyOmit<Todo, 'description' | 'title'>
​
const todo: TodoPreview = {
  completed: false,
}

对象部分属性只读

实现一个泛型MyReadonly2<T, K>,它带有两种类型的参数TK

类型 K 指定 T 中要被设置为只读 (readonly) 的属性。如果未提供K,则应使所有属性都变为只读,就像普通的Readonly<T>一样。

type MyReadonly2<T, K extends keyof T = keyof T> = {
    readonly [key in keyof T as key extends K ? key : never]: T[key]
} & {
    [key in keyof T as key extends K ? never : key]: T[key]
}
​
interface Todo {
  title: string
  description: string
  completed: boolean
}
​
const todo: MyReadonly2<Todo, 'title' | 'description'> = {
  title: "Hey",
  description: "foobar",
  completed: false,
}
​
todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
todo.completed = true // OK

对象属性只读(递归)

实现一个泛型 DeepReadonly<T>,它将对象的每个参数及其子对象递归地设为只读。

您可以假设在此挑战中我们仅处理对象。不考虑数组、函数、类等。但是,您仍然可以通过覆盖尽可能多的不同案例来挑战自己。

type DeepReadonly<T> = keyof T extends never ? T : { readonly [key in keyof T]: DeepReadonly<T[key]> }
​
type X = { 
  x: { 
    a: 1
    b: 'hi'
  }
  y: 'hey'
}
​
type Expected = { 
  readonly x: { 
    readonly a: 1
    readonly b: 'hi'
  }
  readonly y: 'hey' 
}
​
type Todo = DeepReadonly<X> // should be same as `Expected`

元组转集合

实现泛型TupleToUnion<T>,它返回元组所有值的合集。

type TupleToUnion<T extends readonly unknown[]> = T[number]
​
type Arr = ['1', '2', '3']
​
type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'

最后一个元素

实现一个Last<T>泛型,它接受一个数组T并返回其最后一个元素的类型。

type Last<T extends readonly unknown[]> = [never, ...T][T["length"]];
​
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
​
type tail1 = Last<arr1> // 应推导出 'c'
type tail2 = Last<arr2> // 应推导出 1

排除最后一项

实现一个泛型Pop<T>,它接受一个数组T,并返回一个由数组T的前 N-1 项(N 为数组T的长度)以相同的顺序组成的数组。

type Pop<T extends any[]> = T extends [...infer I, infer _] ? I : never
​
type arr1 = ['a', 'b', 'c', 'd']
type arr2 = [3, 2, 1]
​
type re1 = Pop<arr1> // expected to be ['a', 'b', 'c']
type re2 = Pop<arr2> // expected to be [3, 2]

查找类型

有时,您可能希望根据某个属性在联合类型中查找类型。

在此挑战中,我们想通过在联合类型Cat | Dog中通过指定公共属性type的值来获取相应的类型。换句话说,在以下示例中,LookUp<Dog | Cat, 'dog'>的结果应该是DogLookUp<Dog | Cat, 'cat'>的结果应该是Cat

type LookUp<T, V> = T extends { type: V } ? T : never
​
interface Cat {
  type: 'cat'
  breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal'
}
​
interface Dog {
  type: 'dog'
  breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer'
  color: 'brown' | 'white' | 'black'
}
​
type MyDog = LookUp<Cat | Dog, 'dog'> // expected to be `Dog`

去除左侧空白

实现 TrimLeft<T> ,它接收确定的字符串类型并返回一个新的字符串,其中新返回的字符串删除了原字符串开头的空白字符串。

type Space = ' ' | '\n' | '\t'
type TrimLeft<S extends string> = S extends `${Space}${infer A}` ? TrimLeft<A> : Stype trimed = TrimLeft<'  Hello World  '> // 应推导出 'Hello World  '

去除两端空白

type SpaceX = ' ' | '\n' | '\t'
type Trim<S extends string> = S extends `${SpaceX}${infer A}${SpaceX}` ? Trim<A> : Stype trimed = Trim<'  Hello World  '> // expected to be 'Hello World'

Capitalize 字符串首字母大写

实现 Capitalize<T> 它将字符串的第一个字母转换为大写,其余字母保持原样。

type Capitalize1<S extends string> = S extends `${infer A}${infer B}` ? `${Uppercase<A>}${B}` : Stype capitalized = Capitalize<'hello world'> // expected to be 'Hello world'

Replace

实现 Replace<S, From, To> 将字符串 S 中的第一个子字符串 From 替换为 To

type Replace<S extends string, From extends string, To extends string> =
    From extends '' ? S : S extends `${infer L}${From}${infer R}` ? `${L}${To}${R}` : Stype replaced = Replace<'types are fun!', 'fun', 'awesome'> // 期望是 'types are awesome!'

ReplaceAll

实现 ReplaceAll<S, From, To> 将一个字符串 S 中的所有子字符串 From 替换为 To

type ReplaceAll<S extends string, From extends string, To extends string> = From extends ''
  ? S
  : S extends `${infer R1}${From}${infer R2}`
  ? `${R1}${To}${ReplaceAll<R2, From, To>}`
  : Stype replaced = ReplaceAll<'t y p e s', ' ', ''> // 期望是 'types'

追加参数

实现一个泛型 AppendArgument<Fn, A>,对于给定的函数类型 Fn,以及一个任意类型 A,返回一个新的函数 GG 拥有 Fn 的所有参数并在末尾追加类型为 A 的参数。

type AppendArgument<F, A> = F extends (...args: infer Args) => infer R ? (x: A, ...args: Args) => R : nevertype Fn = (a: number, b: string) => numbertype Result = AppendArgument<Fn, boolean> 
// 期望是 (a: number, b: string, x: boolean) => number

LengthOfString

计算字符串的长度,类似于 String#length

type LengthOfString<
  S extends string,
  T extends string[] = []
> = S extends `${infer F}${infer R}`
  ? LengthOfString<R, [...T, F]>
  : T['length'];
type a = LengthOfString<'xiaoming'>

AppendToObject

实现一个为接口添加一个新字段的类型。该类型接收三个参数,返回带有新字段的接口类型。

type AppendToObject<T, U extends (string | number | symbol), V> = {
    [key in keyof T | U]: key extends keyof T ? T[key] : V
}
type Test = { id: '1' }
type Result = AppendToObject<Test, 'value', 4> // expected to be { id: '1', value: 4 }

Absolute

实现一个接收string,number或bigInt类型参数的Absolute类型,返回一个正数字符串。

type Absolute<T extends string | number | bigint> = `${T}` extends `-${infer U}` ? U : `${T}`
​
type Test = -100;
type Result = Absolute<Test>; // expected to be "100"

StringToUnion

实现一个将接收到的String参数转换为一个字母Union的类型

type StringToUnion<T extends string, V extends string[] = []> = T extends `${infer Letter}${infer Rest}`
    ? StringToUnion<Rest, [...V, Letter]>
    : V[number];
​
type Test = '123';
type Result = StringToUnion<Test>; // expected to be "1" | "2" | "3"

Merge

将两个类型合并成一个类型,第二个类型的键会覆盖第一个类型的键。

type Merge<T, U> = {
    [key in keyof T | keyof U]: key extends keyof U ? U[key] : key extends keyof T ? T[key] : never
}
​
type foo = {
    name: string;
    age: string;
}
​
type coo = {
    age: number;
    sex: string
}
​
type Result = Merge<foo, coo>; // expected to be {name: string, age: number, sex: string}

Diff

获取两个接口类型中的差值属性

/**
 * 思路
 * keyof (Foo | Bar) // "a" 或(|)运算符,拿到的是共有属性
 * keyof (Foo & Bar) // "a"|"b"|"c" 且(&)运算符,拿到的是对象的所有属性
 * 然后用Omit从所有属性中剔除共有属性
 */
type Diff<T, V> = Omit<T & V, keyof (T | V)>
​
type Foo = {
    a: string;
    b: number;
}
type Bar = {
    a: string;
    c: boolean
}
​
type Result1 = Diff<Foo, Bar> // { b: number, c: boolean }
type Result2 = Diff<Bar, Foo> // { b: number, c: boolean }
​

AnyOf

在类型系统中实现类似于 Python 中 any 函数。类型接收一个数组,如果数组中任一个元素为真,则返回 true,否则返回 false。如果数组为空,返回 false

type AnyOf<T extends unknown[]> = T[number] extends '' | 0 | false | [] | { [key: string]: never } ? false : truetype Sample1 = AnyOf<[1, '', false, [], {}]> // expected to be true.
type Sample2 = AnyOf<[0, '', false, [], {}]> // expected to be false.

StartsWith

实现StartsWith<T, U>,接收两个string类型参数,然后判断T是否以U开头,根据结果返回truefalse

type StartsWith<T extends string, U extends string> = T extends `${U}${infer Reset}` ? true : falsetype a = StartsWith<'abc', 'ac'> // expected to be false
type b = StartsWith<'abc', 'ab'> // expected to be true
type c = StartsWith<'abc', 'abcd'> // expected to be false

DropChar

从字符串中剔除指定字符。

type DropChar<S extends string, V extends string> = S extends `${infer F}${infer R}` ? F extends V ? `${DropChar<R, V>}` : `${F}${DropChar<R, V>}` : Stype Butterfly = DropChar<' b u t t e r f l y ! ', ' '> // 'butterfly!'

PickByType

从T中,选择一组类型可分配给U的属性。

type PickByType<T, V> = {
    [K in keyof T as T[K] extends V ? K : never]: T[K]
}
​
type OnlyBoolean = PickByType<{
    name: string
    count: number
    isReadonly: boolean
    isEnable: boolean
}, boolean> // { isReadonly: boolean; isEnable: boolean; }

EndsWith

实现EndsWith<T, U>,接收两个string类型参数,然后判断T是否以U结尾,根据结果返回truefalse

type EndsWith<S extends string, E extends string> = S extends `${infer F}${E}` ? true : falsetype a = EndsWith<'abc', 'bc'> // expected to be true
type b = EndsWith<'abc', 'abc'> // expected to be true
type c = EndsWith<'abc', 'd'> // expected to be false
​
​

PartialByKeys

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

K指定应设置为可选的T的属性集。当没有提供K时,它就和普通的Partial<T>一样使所有属性都是可选的。

/**
 * 思路
 * ① Partial<Pick<T, U & keyof T>>通过Pick拿到需要转换为可选属性的key,Partial转换需要转换的key
 * ② Omit<T, U & keyof T>拿到除开需要转换可选的key之外的其他属性
 * ③ Omit<①&②,never>通过第一步和第二步&运算符拿到所有对象的属性,never不剔除任何属性
 */
type PartialByKeys<T extends {}, U = keyof T> = Omit<Partial<Pick<T, U & keyof T>> & Omit<T, U & keyof T>, never>;
​
interface User {
    name: string
    age: number
    address: string
}
​
type UserPartialName = PartialByKeys<User, 'name'> // { name?:string; age:number; address:string }

RequiredByKeys

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

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

/**
 * 思路
 * ① Required<Pick<T, U & keyof T>>通过Pick拿到需要转换为可选属性的key,Required转换需要转换的key
 * ② Omit<T, U & keyof T>拿到除开需要转换可选的key之外的其他属性
 * ③ Omit<①&②,never>通过第一步和第二步&运算符拿到所有对象的属性,never不剔除任何属性
 */
type RequiredByKeys<T extends {}, K = keyof T> = Omit<Required<Pick<T, K & keyof T>> & Omit<T, K & keyof T>, never>
​
interface User {
    name?: string
    age?: number
    address?: string
}
​
type UserRequiredName = RequiredByKeys<User, 'name'> // { name: string; age?: number; address?: string }

Mutable

实现一个通用的类型 Mutable<T>,使类型 T 的全部属性可变(非只读)。

/**
 * 思路
 * 把一个对象属性设置为只读只需要在前面加上readonly
 * 而把一个对象属性设置为可写在前面加上-readonly
 */
type Mutable<T> = {
    -readonly [K in keyof T]: T[K]
}
​
interface Todo {
    readonly title: string
    readonly description: string
    readonly completed: boolean
}
​
type MutableTodo = Mutable<Todo> // { title: string; description: string; completed: boolean; }
​

OmitByType

从T中,选择一组类型不能分配给U的属性。

/**
 * 思路
 * ① K in keyof T as T[K] extends U?never:K把这段拆成2部分
 * ② (K in keyof T) as (T[K] extends U?never:K)
 * ③ K in keyof T 是遍历的意思,这个好理解
 * ④ as 在这里姑且认定为是 断言
 * ⑤ T[K] extends U?never:K 这里是为了判断类型
 * 连起来看
 * ① K 在 T 的范围循环
 * ② K 得到的是 T 中的键,T[K]得到的是键的类型
 * ③ 对于这个 K 我们为他断言 为 K / never
 * ④ 如果 P 的这个键在 K 的范围中,我们就断言当前的 P 是 never(抛弃原先 P 的值),那么在对象循环的时候 never 就会被忽略掉,从而实现 OmitByType
 */
type OmitByType<T, U> = {
    [K in keyof T as T[K] extends U ? never : K]: T[K]
}
type OmitBoolean = OmitByType<{
    name: string
    count: number
    isReadonly: boolean
    isEnable: boolean
}, boolean> // { name: string; count: number }
​

ObjectEntries

实现返回联合类型Object.entries类似的功能

/**
 * 思路
 * 如何将对象转换成联合类型
 * ['1', '2']['number'] // '1' | '2' 数组转联合类型用 [number] 作为下标
 * type ObjectToUnion<T> = T[keyof T] 对象则是用 [keyof T] 作为下标
 */
type ObjectEntries<T> = {
    [K in keyof Required<T>]: [K, [T[K]] extends [undefined] ? undefined : Required<T>[K]]
}[keyof T]
​
interface Model {
    name?: string;
    age?: number;
    locations: string[] | null;
}
​
type a = undefined extends undefined ? undefined : never
type modelEntries = ObjectEntries<Model> // ['name', string] | ['age', number] | ['locations', string[] | null];

shift

实现类型版本的 Array.shift

/**
 * 关键字infer
 * infer 的作用是让 TypeScript 自己推断,并将推断的结果存储到一个类型变量中
 * infer F是数组的第一个元素
 * ...infer R是数组的剩余元素
 * T extends [infer F, ...infer R]这句话的意思是如果T是[infer F, ...infer R]这个类型或者它的自元素
 * 返回R,也就是除数组第一个元素以外的其他元素,否者不返回
 */
type Shift<T extends unknown[]> = T extends [infer F, ...infer R] ? R : never
​
type Result = Shift<[3, 2, 1]> // [2, 1]

TupleToNestedObject

把元组转换成一个嵌套对象。

/**
 * 思路
 * 通过extends关键字判断T 是否是类型 [infer F, ...infer R]
 * infer关键字解构声明数组的第一项数据,以及剩余数据,如果是空数组,解构不成立,返回类U本身
 * 如果extends成立,把F当作对象的键,值则递归调用TupleToNestedObject得到
 */
type TupleToNestedObject<T, U> = T extends [infer F, ...infer R] ?
    {
        [K in F & string]: TupleToNestedObject<R, U>
    }
    : U
type a = TupleToNestedObject<['a'], string> // {a: string}
type b = TupleToNestedObject<['a', 'b'], number> // {a: {b: number}}
type c = TupleToNestedObject<[], boolean> // boolean. if the tuple is empty, just return the U type
​

Reverse

实现类型版本的数组反转 Array.reverse

type Reverse<T> = T extends [infer F, ...infer R] ? [...Reverse<R>, F] : T
​
type a = Reverse<['a', 'b']> // ['b', 'a']
type b = Reverse<['a', 'b', 'c',]> // ['c', 'b', 'a']

FlipArguments

类型翻转参数需要函数类型T,并返回一个新的函数类型,它具有相同的返回类型T,但与参数相反。

/**
 * 思路
 * 通过T extends (...arg: infer Arg) => infer R 拿到参数,以及返回值
 * 通过自定义Reverse反转参数返回
 */
type Reverse<T extends unknown[]> = T extends [infer F, ...infer R] ? [...Reverse<R>, F] : T
type FlipArguments<T extends (...arg: any[]) => any> = T extends (...arg: infer Arg) => infer R ? (...agr: Reverse<Arg>) => R : never
​
type Flipped = FlipArguments<(arg0: string, arg1: number, arg2: boolean) => void>
// (arg0: boolean, arg1: number, arg2: string) => void