TS体操

175 阅读7分钟

easy

1. 实现pick

实现 TS 内置的 Pick<T, 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,
}

题解

type MyPick<T, U extends keyof T> = { [K in U]: T[K] }

相关知识

keyof 用于获取某种类型的所有键,其返回类型是联合类型。

2. 实现 Readonly

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

该 Readonly 会接收一个 泛型参数,并返回一个完全一样的类型,只是所有属性都会被 readonly 所修饰。

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

例如:

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

题解

type MyReadOnly<T> = { readonly [K in keyof T] : T[K] }

3. 元组转换为对象

传入一个元组类型,将这个元组类型转换为对象类型,这个对象类型的键/值都是从元组中遍历出来。

例如:

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

const result: TupleToObject<typeof tuple> // expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

题解

type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [K in T[number]]: K
}

相关知识

as const相关知识:

杀手级的TypeScript功能:const断言
官方文档

4. 第一个元素

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

例如:

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3

题解

因为存在空数组的情况,所以我们需要考虑never[]

type First<T extends any[]> = T extends never[] ? never : T[0]

相关知识

never 类型表示的是那些永不存在的值的类型。

5. 获取元组长度

创建一个通用的Length,接受一个readonly的数组,返回这个数组的长度。

例如:

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

题解

type Length<T extends readonly any[]> = T['length']

6. Exclude

实现内置的Exclude <T,U>

从T中排除可分配给U的那些类型

如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T 类型。最终实现的效果就是将 T 中某些属于 U 的类型移除掉

示例

type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

题解

如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T 类型。最终实现的效果就是将 T 中某些属于 U 的类型移除掉。

待证实:如果T是联合类型,比如"a" | "b" | "c",运行时为 "a" extends U, "b" extends U, "c" extends U,

type MyExclude<T, U> = T extends U ? never: T

7. Awaited

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

比如:Promise<ExampleType>,请你返回 ExampleType 类型。

测试用例

import { Equal, Expect } from '@type-challenges/utils'

type X = Promise<string>
type Y = Promise<{ field: number }>

type cases = [
  Expect<Equal<Awaited<X>, string>>,
  Expect<Equal<Awaited<Y>, { field: number }>>,
]

// @ts-expect-error
type error = Awaited<number>

题解

type Awaited<T extends Promise<any>> = T extends Promise<infer P> ? P: never;

相关知识

infer 声明一个类型变量并且对它进行使用

8. If

实现一个 If,接受布尔类型 C,如果为 true 返回类型 Tfalse 返回类型的 F。同时TF可以是任何类型。

例如

type A = If<true, 'a', 'b'>  // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'

题解

type If<C extends boolean, T, F> = C extends true ? T : F;

9. Concat

实现 JavaScript Array.concat 函数。 接受两个参数,输出应该是一个新数组,按照输入顺序排序。

示例

type Result = Concat<[1], [2]> // expected to be [1, 2]

题解

type Concat<T extends any[], U extends any[]> = [...T, ...U]

10. Includes

实现 JavaScript Array.includes函数。接受两个参数,输出应该是一个布尔值truefalse

示例

type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`

题解

// 乞丐版
type Includes<T extends readonly any[], U> = U extends T[number] ? true : false;

// 完整版
type IsEqual<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? true : false;

type Includes<T extends readonly any[], U> = T extends [infer P, ...infer Rest] ? (IsEqual<P, U> extends true ? true : Includes<Rest, U>) : false

github.com/type-challe…

11. push

实现 Array.push

示例

type Result = Push<[1, 2], '3'> // [1, 2, '3']

题解

type Push<T extends any[], U> = [...T, U]

12. Unshift

实现 Array.unshift

示例

type Result = Unshift<[1, 2], 0> // [0, 1, 2,]

题解

type Unshift<T extends any[], U> = [U, ...T]

13. Parameters

获取函数入参

测试用例

import { Equal, Expect, ExpectFalse, NotEqual } from '@type-challenges/utils'

const foo = (arg1: string, arg2: number): void => {}
const bar = (arg1: boolean, arg2: {a: 'A'}): void => {}
const baz = (): void => {}

type cases = [
  Expect<Equal<MyParameters<typeof foo>, [string, number]>>,
  Expect<Equal<MyParameters<typeof bar>, [boolean, {a: 'A'}]>>,
  Expect<Equal<MyParameters<typeof baz>, []>>,
]

题解

type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never

medium

1. 获取函数返回类型

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

例如:

const fn = (v: boolean) => {
  if (v)
    return 1
  else
    return 2
}

type a = MyReturnType<typeof fn> // 应推导出 "1 | 2"

题解

type MyReturnType<T> = T extends (...args: any) => infer P ? P : never

2. 实现 Omit

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

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

例如:

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyOmit<Todo, 'description' | 'title'>

const todo: TodoPreview = {
  completed: false,
}

题解

// 1
type MyOmit<T extends object, U extends keyof T> = {
  [K in keyof T as (K extends U ? never : K)
  ]: T[K]
}

// 2
type MyExclude<T, U> = T extends U ? never : T;
type MyOmit<T, U extends keyof T> = { [K in MyExclude<keyof T, U>]: T[K] };

// 3
// 使用Pick与Exclude 这两个 Utility Types
type MyOmit<T, K> = Pick<T,Exclude<keyof T,K>>

3. Readonly 2

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

K指定应设置为Readonly的T的属性集。如果未提供K,则应使所有属性都变为只读,就像普通的Readonly<T>一样。

例如

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

const todo2: MyReadonly2<Todo> = {
  title: "Hey",
  description: "foobar",
  completed: false,
}

todo2.title = "Hello" // Error: cannot reassign a readonly property
todo2.description = "barFoo" // Error: cannot reassign a readonly property
todo2.completed = true // Error: cannot reassign a readonly property

题解

// = keyof T代表如果第二个参数不传,默认为keyof T
type MyReadonly2<T, K extends keyof T = keyof T> = T & {
  readonly [P in K] : T[P]
}

4. 深度 Readonly

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

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

例如

type X = { 
  x: { 
    a: 1
    b: 'hi'
  }
  y: 'hey'
}

type Expected = { 
  readonly x: { 
    readonly a: 1
    readonly b: 'hi'
  }
  readonly y: 'hey' 
}

const todo: DeepReadonly<X> // should be same as `Expected`

题解

// TODO
type DeepReadonly<T> = T extends { [propName: string]: infer R } 
    ? { readonly [i in keyof T]: DeepReadonly<T[i]> } 
    : T;

5. 元组转合集

实现泛型TupleToUnion<T>,它覆盖元组的值与其值联合。

例如

type Arr = ['1', '2', '3']

const a: TupleToUnion<Arr> // expected to be '1' | '2' | '3'

题解

// TODO
type TupleToUnion<T extends any[]> = T[number];

type TupleToUnion<T> = T extends any[] ? T[number] : never;

6. 最后一个元素

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

例如

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type tail1 = Last<arr1> // expected to be 'c'
type tail2 = Last<arr2> // expected to be 1

题解

type Last<T extends any[]> = T extends [...any, infer L] ? L : never;

7. 出堆

实现一个通用Pop<T>,它接受一个数组T并返回一个没有最后一个元素的数组。

例如

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]

额外:同样,您也可以实现ShiftPushUnshift吗?

题解

type Pop<T extends any[]> = T extends [...infer R, any] ? R : never;

8. Promise.all

键入函数PromiseAll,它接受PromiseLike对象数组,返回值应为Promise<T>,其中T是解析的结果数组。

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise<string>((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

// expected to be `Promise<[number, number, string]>`
const p = Promise.all([promise1, promise2, promise3] as const)

题解

// TODO
declare function PromiseAll<T extends any[]>(
  values: readonly [...T]
): Promise<{
  [key in keyof T]: T[key] extends PromiseLike<infer V> ? V : T[key];
}>;

9. Type Lookup

有时,您可能希望根据其属性在并集中查找类型。

在此挑战中,我们想通过在联合Cat | Dog中搜索公共type字段来获取相应的类型。换句话说,在以下示例中,我们期望LookUp<Dog | Cat, 'dog'>获得DogLookUp<Dog | Cat, 'cat'>获得Cat

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`

题解

type LookUp<T extends { type: any }, U extends T["type"]> = T extends {
  type: U;
}
  ? U
  : never;
  
type LookUp<U, T> = U extends {type: any} ? (U['type'] extends T ? U : never) : never;

type LookUp<U, T> = U extends {type:infer R} ? R extends T ? U : never : never;