Skip to content
大纲

TS 练习题

做练习题,巩固基础 💪

重学TS 2.0 系列

「重学TS 2.0 」 系列练习题

第一题

题目

ts
type User = {
  id: number;
  kind: string;
};

function makeCustomer<T extends User>(u: T): T {
  // Error(TS 编译器版本:v4.4.2)
  // Type '{ id: number; kind: string; }' is not assignable to type 'T'.
  // '{ id: number; kind: string; }' is assignable to the constraint of type 'T', 
  // but 'T' could be instantiated with a different subtype of constraint 'User'.
  return {
    id: u.id,
    kind: 'customer'
  }
}

以上代码为什么会提示错误,应该如何解决上述问题?

解答

为什么报错?

  • 因为 T 兼容 User 类型,即包含但不限于 User 类型,所以返回为 T 类型可能不仅仅只有 idkind,所以要么修改返回值类型,要么就修改返回值
  1. 修改返回值类型:
ts
type User = {
  id: number;
  kind: string;
};

function makeCustomer<T extends User>(u: T): User {
  return {
    id: u.id,
    kind: 'customer'
  }
}
  1. 修改返回值:
ts
type User = {
  id: number;
  kind: string;
};

function makeCustomer<T extends User>(u: T): T {
  return {
    ...u,
    id: u.id,
    kind: 'customer'
  }
}

第二题

题目

本道题我们希望参数 a 和 b 的类型都是一致的,即 a 和 b 同时为 number 或 string 类型。当它们的类型不一致的值,TS 类型检查器能自动提示对应的错误信息。

ts
function f(a: string | number, b: string | number) {
  if (typeof a === 'string') {
    return a + ':' + b; // no error but b can be number!
  } else {
    return a + b; // error as b can be number | string
  }
}

f(2, 3); // Ok
f(1, 'a'); // Error
f('a', 2); // Error
f('a', 'b') // Ok

解答

  1. 使用函数重载:
ts
function f(a: number, b: number): number;
function f(a: string, b: string): string;
function f(a, b) {
  if (typeof a === 'string') {
    return a + ':' + b; // no error but b can be number!
  } else {
    return a + b; // error as b can be number | string
  }
}
f(2, 3); // Ok
f(1, 'a'); // Error
f('a', 2); // Error
f('a', 'b') // Ok
  1. 泛型解决
ts
function f<T extends string | number>(a: T, b: T) {
  if (typeof a === 'string') {
    return a + ':' + b; // no error but b can be number!
  } else {
    return (a as number) + (b as number); // error as b can be number | string
  }
}
f(2, 3); // Ok
f(1, 'a'); // Error
f('a', 2); // Error
f('a', 'b') // Ok

第三题

题目

掌握 TS 这些工具类型,让你开发事半功倍 这篇文章中,阿宝哥介绍了 TS 内置的工具类型:Partial<T>,它的作用是将某个类型里的属性全部变为可选项 ?

ts
interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

// lib.es5.d.ts
type Partial<T> = {
  [P in keyof T]?: T[P];
};

那么如何定义一个 SetOptional 工具类型,支持把给定的 keys 对应的属性变成可选的?对应的使用示例如下所示:

ts
type Foo = {
	a: number;
	b?: string;
	c: boolean;
}

// 测试用例
type SomeOptional = SetOptional<Foo, 'a' | 'b'>;

// type SomeOptional = {
// 	a?: number; // 该属性已变成可选的
// 	b?: string; // 保持不变
// 	c: boolean; 
// }

在实现 SetOptional 工具类型之后,如果你感兴趣,可以继续实现 SetRequired 工具类型,利用它可以把指定的 keys 对应的属性变成必填的。对应的使用示例如下所示:

ts
type Foo = {
	a?: number;
	b: string;
	c?: boolean;
}

// 测试用例
type SomeRequired = SetRequired<Foo, 'b' | 'c'>;
// type SomeRequired = {
// 	a?: number;
// 	b: string; // 保持不变
// 	c: boolean; // 该属性已变成必填
// }

解答

SetOptional 思路:

  • 先使用 Omit<Foo, 'a' | 'b'> 得到排除了 a | bFoo,再使用 Partial<Pick<Foo, 'a' | 'b'>>Foo 中的 a | b 设置为可选,最后将两种类型合并 & 返回即可。
ts
type SetOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

SetRequired 思路:

  • SetOptional,只不过将后面的 Partial 改为 Required 即可。
ts
type SetRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

第四题

题目

Pick<T, K extends keyof T> 的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。

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

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false
};

那么如何定义一个 ConditionalPick 工具类型,支持根据指定的 Condition 条件来生成新的类型,对应的使用示例如下:

ts
interface Example {
	a: string;
	b: string | number;
	c: () => void;
	d: {};
}

// 测试用例:
type StringKeysOnly = ConditionalPick<Example, string>;
//=> {a: string}

解答

  • [K in keyof T as (T[K] extends U ? K : never)]: 这里使用 as 巧妙的对 T[K] 进行二次判断,使 : 左边返回符合 T[K] extends UK

  • 还有一个知识点:当 key 为 never nullundefined 时不会生效。

ts
type ConditionalPick<T, U> = {
  [K in keyof T as (T[K] extends U ? K : never)]: T[K]
}

interface Example {
	a: string;
	b: string | number;
	c: () => void;
	d: {};
}

// 测试用例:
type StringKeysOnly = ConditionalPick<Example, string>;
//=> {a: string}

第五题

题目

定义一个工具类型 AppendArgument,为已有的函数类型增加指定类型的参数,新增的参数名是 x,将作为新函数类型的第一个参数。具体的使用示例如下所示:

ts
type Fn = (a: number, b: string) => number
type AppendArgument<F, A> = // 你的实现代码

type FinalFn = AppendArgument<Fn, boolean> 
// (x: boolean, a: number, b: string) => number

解答

  1. 使用 Parameters<F>ReturnType<F> 工具类型提取出 Fn 函数类型的参数类型和返回值类型,再与 x: A 组合成新的类型即可
ts
// 使用 Parameters<F> 和 ReturnType<F> 工具类型
type AppendArgument<F extends (...args: any) => any, A> = (x: A, ...args: Parameters<F>) => ReturnType<F>;

type Fn = (a: number, b: string) => number

type FinalFn = AppendArgument<Fn, boolean> 
// (x: boolean, a: number, b: string) => number
  1. 使用 infer,因为 ParametersReturnType 都是通过 infer 实现的;这里直接使用 infer,在一个函数类型里声明了两个变量,即可同时收集参数类型和返回值类型
ts
// 使用 infer
type AppendArgument<F extends (...args: any) => any, T> = F extends (...args: infer P) => infer R ? (x: T, ...args: P) => R : never;

type Fn = (a: number, b: string) => number

type FinalFn = AppendArgument<Fn, boolean> 
// (x: boolean, a: number, b: string) => number

第六题

题目

定义一个 NativeFlat 工具类型,支持把数组类型拍平(扁平化)。具体的使用示例如下所示:

ts
type NaiveFlat<T extends any[]> = // 你的实现代码

// 测试用例:
type NaiveResult = NaiveFlat<[['a'], ['b', 'c'], ['d']]>
// NaiveResult的结果: "a" | "b" | "c" | "d"

在完成 NaiveFlat 工具类型之后,在继续实现 DeepFlat 工具类型,以支持多维数组类型:

ts
type DeepFlat<T extends any[]> = unknown // 你的实现代码

// 测试用例
type Deep = [['a'], ['b', 'c'], [['d']], [[[['e']]]]];
type DeepTestResult = DeepFlat<Deep>  
// DeepTestResult: "a" | "b" | "c" | "d" | "e"

知识点铺垫

  1. ['a', 'b', 'c'][number] 可以取出 "a" | "b" | "c"。演变过程如下:
ts
// 使用下标取数组元素
type A = ['a', 'b', 'c'][0] // type A = "a"

// | 可以取多个下标
type B = ['a', 'b', 'c'][0 | 1] // type B = "a" | "b"

// number 表示取所有下标,即取出数组中所有的类型
type C = ['a', 'b', 'c'][number] // type C = "a" | "b" | "c"
  1. {[K in keyof ['a', 'b', 'c']]} 得到 {0: 'a', 1: 'b', 2: 'c', length: 3, ...}, 再加上 [number] 就可以提取出数组的联合类型:
ts
type StringArr = ['a', 'b', 'c'];
// 根据数组类型生成对象类型
type KeyofArrMap = {
  [K in keyof StringArr]: StringArr[K]
}
/* 这里会将 Array 的属性都暴露出来
type KeyofArrMap = {
    [x: number]: "a" | "b" | "c";
    0: "a";
    1: "b";
    2: "c";
    length: 3;
    toString: () => string;
    toLocaleString: () => string;
    pop: () => "a" | "b" | "c";
    ...
}
*/

// 加上 [number] 即可将所有数字所有对应的值提取出来
type KeyofArrType = KeyofArrMap[number] // type KeyofArrType = "a" | "b" | "c"

解答

NaiveFlat 实现:

ts
type NaiveFlat<T extends any[]> = {
  [P in keyof T]: T[P] extends any[] ? T[P][number] : T[P]
}[number]

// 测试用例:
type NaiveResult = NaiveFlat<[['a'], ['b', 'c'], ['d']]>
// NaiveResult的结果: "a" | "b" | "c" | "d"

DeepFlat 实现:

ts
type DeepFlat<T extends any[]> = {
  [P in keyof T]: T[P] extends any[] ? DeepFlat<T[P]> : T[P]
}[number]

// 测试用例
type Deep = [['a'], ['b', 'c'], [['d']], [[[['e']]]]];
type DeepTestResult = DeepFlat<Deep>  
// DeepTestResult: "a" | "b" | "c" | "d" | "e"

第七题

题目

使用类型别名定义一个 EmptyObject 类型,使得该类型只允许空对象赋值:

ts
type EmptyObject = {} 

// 测试用例
const shouldPass: EmptyObject = {}; // 可以正常赋值
const shouldFail: EmptyObject = { // 将出现编译错误
  prop: "TS"
}

在通过 EmptyObject 类型的测试用例检测后,我们来更改以下 takeSomeTypeOnly 函数的类型定义,让它的参数只允许严格SomeType类型的值。具体的使用示例如下所示:

ts
type SomeType =  {
  prop: string
}

// 更改以下函数的类型定义,让它的参数只允许严格SomeType类型的值
function takeSomeTypeOnly(x: SomeType) { return x }

// 测试用例:
const x = { prop: 'a' };
takeSomeTypeOnly(x) // 可以正常调用

const y = { prop: 'a', addditionalProp: 'x' };
takeSomeTypeOnly(y) // 将出现编译错误

知识点铺垫

PropertyKey 系统内置索引签名参数类型,源码:

ts
type PropertyKey = string | number | symbol

解答

EmptyObject

ts
type EmptyObject = {
  // type PropertyKey = string | number | symbol
  [K in PropertyKey]: never 
}

// 测试用例
const shouldPass: EmptyObject = {}; // 可以正常赋值
const shouldFail: EmptyObject = { // 将出现编译错误
  prop: "TS"
}

takeSomeTypeOnly

ts
type SomeType =  {
  prop: string
}

// 将 T2 中和 T1 不相同的 K 设置为 never
type Exclusive<T1, T2 extends T1> = {
  [K in keyof T2]: K extends keyof T1 ? T2[K] : never 
}

// 更改以下函数的类型定义,让它的参数只允许严格SomeType类型的值
function takeSomeTypeOnly<T extends SomeType>(x: Exclusive<SomeType, T>) { return x }

// 测试用例:
const x = { prop: 'a' };
takeSomeTypeOnly(x) // 可以正常调用

const y = { prop: 'a', addditionalProp: 'x' };
takeSomeTypeOnly(y) // 将出现编译错误

第八题

题目

定义 NonEmptyArray 工具类型,用于确保数据非空数组。

ts
type NonEmptyArray<T> = // 你的实现代码

const a: NonEmptyArray<string> = [] // 将出现编译错误
const b: NonEmptyArray<string> = ['Hello TS'] // 非空数据,正常使用

解答

解法一

ts
type NonEmptyArray<T> = [T, ...T[]]

解法二

ts
type NonEmptyArray<T> = T[] & { 0: T }

@antfu TS类型体操

TypeScript 类型体操姿势合集练习题

简单(13题)

实现 Pick

实现 TS 内置的 Pick<T, K>,但不可以使用它。

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

例如:

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

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}

Playground

解答:

ts
/*
  4 - 实现 Pick
  -------
  by Anthony Fu (@antfu) #简单 #union #built-in

  ### 题目

  > 欢迎 PR 改进翻译质量。

  实现 TS 内置的 `Pick<T, K>`,但不可以使用它。

  **从类型 `T` 中选择出属性 `K`,构造成一个新的类型**。

  例如:

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

  type TodoPreview = MyPick<Todo, 'title' | 'completed'>

  const todo: TodoPreview = {
      title: 'Clean room',
      completed: false,
  }
  ``

  > 在 Github 上查看:https://tsch.js.org/4/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Expected1, MyPick<Todo, 'title'>>>,
  Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>,
  // @ts-expect-error
  MyPick<Todo, 'title' | 'completed' | 'invalid'>,
]

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

interface Expected1 {
  title: string
}

interface Expected2 {
  title: string
  completed: boolean
}

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/4/answer/zh-CN
  > 查看解答:https://tsch.js.org/4/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

实现 Readonly

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

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

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

例如:

ts
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

Playground

解答:

ts
/*
  7 - 实现 Readonly
  -------
  by Anthony Fu (@antfu) #简单 #built-in #readonly #object-keys

  ### 题目

  > 欢迎 PR 改进翻译质量。

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

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

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

  例如:

  ``ts
  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
  ``

  > 在 Github 上查看:https://tsch.js.org/7/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<MyReadonly<Todo1>, Readonly<Todo1>>>,
]

interface Todo1 {
  title: string
  description: string
  completed: boolean
  meta: {
    author: string
  }
}
/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/7/answer/zh-CN
  > 查看解答:https://tsch.js.org/7/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

元组转换为对象

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

例如:

ts
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'}

Playground

解答:

ts
/*
  11 - 元组转换为对象
  -------
  by sinoon (@sinoon) #简单 #object-keys

  ### 题目

  > 欢迎 PR 改进翻译质量。

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

  例如:

  `ts
  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'}
  `

  > 在 Github 上查看:https://tsch.js.org/11/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
const tupleNumber = [1, 2, 3, 4] as const
const tupleMix = [1, '2', 3, '4'] as const

type cases = [
  Expect<Equal<TupleToObject<typeof tuple>, { tesla: 'tesla'; 'model 3': 'model 3'; 'model X': 'model X'; 'model Y': 'model Y' }>>,
  Expect<Equal<TupleToObject<typeof tupleNumber>, { 1: 1; 2: 2; 3: 3; 4: 4 }>>,
  Expect<Equal<TupleToObject<typeof tupleMix>, { 1: 1; '2': '2'; 3: 3; '4': '4' }>>,
]

// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/11/answer/zh-CN
  > 查看解答:https://tsch.js.org/11/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

第一个元素

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

例如:

ts
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

Playground

解答:

ts
/*
  14 - 第一个元素
  -------
  by Anthony Fu (@antfu) #简单 #array

  ### 题目

  > 欢迎 PR 改进翻译质量。

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

  例如:

  `ts
  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
  `

  > 在 Github 上查看:https://tsch.js.org/14/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<First<[3, 2, 1]>, 3>>,
  Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>,
  Expect<Equal<First<[]>, never>>,
  Expect<Equal<First<[undefined]>, undefined>>,
]

type errors = [
  // @ts-expect-error
  First<'notArray'>,
  // @ts-expect-error
  First<{ 0: 'arrayLike' }>,
]

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/14/answer/zh-CN
  > 查看解答:https://tsch.js.org/14/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

获取元组长度

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

例如:

ts
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

Playground

解答:

ts
/*
  18 - 获取元组长度
  -------
  by sinoon (@sinoon) #简单 #tuple

  ### 题目

  > 欢迎 PR 改进翻译质量。

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

  例如:

  ``ts
  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
  ``

  > 在 Github 上查看:https://tsch.js.org/18/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

const tesla = ['tesla', 'model 3', 'model X', 'model Y'] as const
const spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT'] as const

type cases = [
  Expect<Equal<Length<typeof tesla>, 4>>,
  Expect<Equal<Length<typeof spaceX>, 5>>,
  // @ts-expect-error
  Length<5>,
  // @ts-expect-error
  Length<'hello world'>,
]

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/18/answer/zh-CN
  > 查看解答:https://tsch.js.org/18/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

Exclude

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

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

例如:

ts
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

Playground

解答:

ts
/*
  43 - Exclude
  -------
  by Zheeeng (@zheeeng) #简单 #built-in #union

  ### 题目

  > 欢迎 PR 改进翻译质量。

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

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

  例如:

  ``ts
  type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
  ``

  > 在 Github 上查看:https://tsch.js.org/43/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a'>, 'b' | 'c'>>,
  Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a' | 'b'>, 'c'>>,
  Expect<Equal<MyExclude<string | number | (() => void), Function>, string | number>>,
]

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/43/answer/zh-CN
  > 查看解答:https://tsch.js.org/43/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

Awaited

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

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

ts
type ExampleType = Promise<string>

type Result = MyAwaited<ExampleType> // string

Playground

解答:

ts
/*
  189 - Awaited
  -------
  by Maciej Sikora (@maciejsikora) #简单 #promise #built-in

  ### 题目

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

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

  ``ts
  type ExampleType = Promise<string>

  type Result = MyAwaited<ExampleType> // string
  ``

  > 这个挑战来自于 [@maciejsikora](https://github.com/maciejsikora) 的文章:[original article](https://dev.to/macsikora/advanced-typescript-exercises-question-1-45k4)

  > 在 Github 上查看:https://tsch.js.org/189/zh-CN
*/

/* _____________ 你的代码 _____________ */

type MyAwaited<T extends Pick<Promise<any>, 'then'>> = T extends { then: (onfulfilled: (arg: infer R) => any) => any }
  ? R extends Pick<Promise<any>, 'then'> ? MyAwaited<R> : R
  : T;


/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type X = Promise<string>
type Y = Promise<{ field: number }>
type Z = Promise<Promise<string | number>>
type Z1 = Promise<Promise<Promise<string | boolean>>>
type T = { then: (onfulfilled: (arg: number) => any) => any }

type Te = MyAwaited<T>

type cases = [
  Expect<Equal<MyAwaited<X>, string>>,
  Expect<Equal<MyAwaited<Y>, { field: number }>>,
  Expect<Equal<MyAwaited<Z>, string | number>>,
  Expect<Equal<MyAwaited<Z1>, string | boolean>>,
  Expect<Equal<MyAwaited<T>, number>>,
]

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

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/189/answer/zh-CN
  > 查看解答:https://tsch.js.org/189/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

If

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

例如:

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

Playground

解答:

ts
/*
  268 - If
  -------
  by Pavel Glushkov (@pashutk) #简单 #utils

  ### 题目

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

  例如:

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

  > 在 Github 上查看:https://tsch.js.org/268/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<If<true, 'a', 'b'>, 'a'>>,
  Expect<Equal<If<false, 'a', 2>, 2>>,
]

// @ts-expect-error
type error = If<null, 'a', 'b'>

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/268/answer/zh-CN
  > 查看解答:https://tsch.js.org/268/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

Concat

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

例如:

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

Playground

解答:

ts
/*
  533 - Concat
  -------
  by Andrey Krasovsky (@bre30kra69cs) #简单 #array

  ### 题目

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

  例如:

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

  > 在 Github 上查看:https://tsch.js.org/533/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Concat<[], []>, []>>,
  Expect<Equal<Concat<[], [1]>, [1]>>,
  Expect<Equal<Concat<[1, 2], [3, 4]>, [1, 2, 3, 4]>>,
  Expect<Equal<Concat<['1', 2, '3'], [false, boolean, '4']>, ['1', 2, '3', false, boolean, '4']>>,
]

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/533/answer/zh-CN
  > 查看解答:https://tsch.js.org/533/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

Includes

此题不简单

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

例如:

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

Playground

解答:

ts
/*
  898 - Includes
  -------
  by null (@kynefuk) #简单 #array

  ### 题目

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

  例如:

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

  > 在 Github 上查看:https://tsch.js.org/898/zh-CN
*/

/* _____________ 你的代码 _____________ */

/** 比较两个类型是否相等 */
type IsEqual<T, U> =
	(<G>() => G extends T ? 1 : 2) extends
	(<G>() => G extends U ? 1 : 2)
		? true
		: false;

type Includes<T extends readonly any[], U> = T extends [infer F, ...infer R] 
  ? IsEqual<F, U> extends true 
    ? true : Includes<R, U>
  : false; 

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Kars'>, true>>,
  Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'>, false>>,
  Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 7>, true>>,
  Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 4>, false>>,
  Expect<Equal<Includes<[1, 2, 3], 2>, true>>,
  Expect<Equal<Includes<[1, 2, 3], 1>, true>>,
  Expect<Equal<Includes<[{}], { a: 'A' }>, false>>,
  Expect<Equal<Includes<[boolean, 2, 3, 5, 6, 7], false>, false>>,
  Expect<Equal<Includes<[true, 2, 3, 5, 6, 7], boolean>, false>>,
  Expect<Equal<Includes<[false, 2, 3, 5, 6, 7], false>, true>>,
  Expect<Equal<Includes<[{ a: 'A' }], { readonly a: 'A' }>, false>>,
  Expect<Equal<Includes<[{ readonly a: 'A' }], { a: 'A' }>, false>>,
  Expect<Equal<Includes<[1], 1 | 2>, false>>,
  Expect<Equal<Includes<[1 | 2], 1>, false>>,
  Expect<Equal<Includes<[null], undefined>, false>>,
  Expect<Equal<Includes<[undefined], null>, false>>,
]

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/898/answer/zh-CN
  > 查看解答:https://tsch.js.org/898/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

Push

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

例如:

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

解答:

Playground

ts
/*
  3057 - Push
  -------
  by jiangshan (@jiangshanmeta) #简单 #array

  ### 题目

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

  例如:

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

  > 在 Github 上查看:https://tsch.js.org/3057/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Push<[], 1>, [1]>>,
  Expect<Equal<Push<[1, 2], '3'>, [1, 2, '3']>>,
  Expect<Equal<Push<['1', 2, '3'], boolean>, ['1', 2, '3', boolean]>>,
]

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/3057/answer/zh-CN
  > 查看解答:https://tsch.js.org/3057/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

Unshift

实现类型版本的 Array.unshift

例如:

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

解答:

Playground

ts
/*
  3060 - Unshift
  -------
  by jiangshan (@jiangshanmeta) #简单 #array

  ### 题目

  实现类型版本的 ```Array.unshift```。

  例如:

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

  > 在 Github 上查看:https://tsch.js.org/3060/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Unshift<[], 1>, [1]>>,
  Expect<Equal<Unshift<[1, 2], 0>, [0, 1, 2]>>,
  Expect<Equal<Unshift<['1', 2, '3'], boolean>, [boolean, '1', 2, '3']>>,
]

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/3060/answer/zh-CN
  > 查看解答:https://tsch.js.org/3060/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

Parameters

实现内置的 Parameters<T> 类型,而不是直接使用它,可参考TypeScript官方文档

例如:

ts
const foo = (arg1: string, arg2: number): void => {}

type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]

解答:

Playground

ts
/*
  3312 - Parameters
  -------
  by midorizemi (@midorizemi) #简单 #infer #tuple #built-in

  ### 题目

  实现内置的 Parameters<T> 类型,而不是直接使用它,可参考[TypeScript官方文档](https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterstype)。

  例如:

  ``ts
  const foo = (arg1: string, arg2: number): void => {}

  type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
  ``

  > 在 Github 上查看:https://tsch.js.org/3312/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } 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>, []>>,
]

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/3312/answer/zh-CN
  > 查看解答:https://tsch.js.org/3312/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

中等

获取函数返回类型

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

例如:

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

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

Playground

解答:

ts
/*
  2 - 获取函数返回类型
  -------
  by Anthony Fu (@antfu) #中等 #infer #built-in

  ### 题目

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

  例如:

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

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

  > 在 Github 上查看:https://tsch.js.org/2/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<string, MyReturnType<() => string>>>,
  Expect<Equal<123, MyReturnType<() => 123>>>,
  Expect<Equal<ComplexObject, MyReturnType<() => ComplexObject>>>,
  Expect<Equal<Promise<boolean>, MyReturnType<() => Promise<boolean>>>>,
  Expect<Equal<() => 'foo', MyReturnType<() => () => 'foo'>>>,
  Expect<Equal<1 | 2, MyReturnType<typeof fn>>>,
  Expect<Equal<1 | 2, MyReturnType<typeof fn1>>>,
]

type ComplexObject = {
  a: [12, 'foo']
  bar: 'hello'
  prev(): number
}

const fn = (v: boolean) => v ? 1 : 2
const fn1 = (v: boolean, w: any) => v ? 1 : 2

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/2/answer/zh-CN
  > 查看解答:https://tsch.js.org/2/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

实现 Omit

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

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

例如:

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

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

const todo: TodoPreview = {
  completed: false,
}

Playground

解答:

ts
/*
  3 - 实现 Omit
  -------
  by Anthony Fu (@antfu) #中等 #union #built-in

  ### 题目

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

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

  例如:

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

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

  const todo: TodoPreview = {
    completed: false,
  }
  ``

  > 在 Github 上查看:https://tsch.js.org/3/zh-CN
*/

/* _____________ 你的代码 _____________ */

type MyOmit<T, K extends keyof T> = {
  [P in keyof T as (P extends K ? never : P)]: T[P];
}

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Expected1, MyOmit<Todo, 'description'>>>,
  Expect<Equal<Expected2, MyOmit<Todo, 'description' | 'completed'>>>,
]

// @ts-expect-error
type error = MyOmit<Todo, 'description' | 'invalid'>

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

interface Expected1 {
  title: string
  completed: boolean
}

interface Expected2 {
  title: string
}

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/3/answer/zh-CN
  > 查看解答:https://tsch.js.org/3/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

Readonly

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

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

例如:

ts
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

Playground

解答:

ts
/*
  8 - Readonly 2
  -------
  by Anthony Fu (@antfu) #中等 #readonly #object-keys

  ### 题目

  > 由谷歌自动翻译,欢迎 PR 改进翻译质量。

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

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

  例如

  ``ts
  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
  ``

  > 在 Github 上查看:https://tsch.js.org/8/zh-CN
*/

/* _____________ 你的代码 _____________ */

// 因为第二个泛型可能为空,所以需要通过 = 来赋默认值
// 因为第二个交叉类型使用了 (P extends K ? never : P) 的判断,所以 K 的默认值应该是 keyof T
type MyReadonly2<T, K extends keyof T = keyof T> = {
  readonly [P in K]: T[P];
} & {
  [P in keyof T as (P extends K ? never : P)]: T[P];
}

type t = MyReadonly2<Todo1>

/* _____________ 测试用例 _____________ */
import type { Alike, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Alike<MyReadonly2<Todo1>, Readonly<Todo1>>>,
  Expect<Alike<MyReadonly2<Todo1, 'title' | 'description'>, Expected>>,
  Expect<Alike<MyReadonly2<Todo2, 'title' | 'description'>, Expected>>,
  Expect<Alike<MyReadonly2<Todo2, 'description' >, Expected>>,
]

// @ts-expect-error
type error = MyReadonly2<Todo1, 'title' | 'invalid'>

interface Todo1 {
  title: string
  description?: string
  completed: boolean
}

interface Todo2 {
  readonly title: string
  description?: string
  completed: boolean
}

interface Expected {
  readonly title: string
  readonly description?: string
  completed: boolean
}

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/8/answer/zh-CN
  > 查看解答:https://tsch.js.org/8/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

深度 Readonly

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

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

例如:

ts
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`

Playground

解答:

ts
/*
  9 - 深度 Readonly
  -------
  by Anthony Fu (@antfu) #中等 #readonly #object-keys #deep

  ### 题目

  > 由谷歌自动翻译,欢迎 PR 改进翻译质量。

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

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

  例如

  ``ts
  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`
  ``

  > 在 Github 上查看:https://tsch.js.org/9/zh-CN
*/

/* _____________ 你的代码 _____________ */

// 是否是一个 不是函数 也 不是数组 的 对象
type IsPlainObject<T> = T extends object ?
  T extends Function ? false :
    T extends Array<any> ? false : 
    true : 
  false

type DeepReadonlyArray<T extends any[]> = {
  [K in keyof T]: IsPlainObject<T[K]> extends true ? DeepReadonly<T[K]> : T[K];
}

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends [...infer Values] ? 
    readonly [...DeepReadonlyArray<Values>] : 
    IsPlainObject<T[K]> extends true ? 
      DeepReadonly<T[K]> : 
      T[K]

}

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<DeepReadonly<X1>, Expected1>>,
  Expect<Equal<DeepReadonly<X2>, Expected2>>,
]

type X1 = {
  a: () => 22
  b: string
  c: {
    d: boolean
    e: {
      g: {
        h: {
          i: true
          j: 'string'
        }
        k: 'hello'
      }
      l: [
        'hi',
        {
          m: ['hey']
        },
      ]
    }
  }
}

type X2 = { a: string } | { b: number }

type Expected1 = {
  readonly a: () => 22
  readonly b: string
  readonly c: {
    readonly d: boolean
    readonly e: {
      readonly g: {
        readonly h: {
          readonly i: true
          readonly j: 'string'
        }
        readonly k: 'hello'
      }
      readonly l: readonly [
        'hi',
        {
          readonly m: readonly ['hey']
        },
      ]
    }
  }
}

type Expected2 = { readonly a: string } | { readonly b: number }

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/9/answer/zh-CN
  > 查看解答:https://tsch.js.org/9/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

元组转合集

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

例如:

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

type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'

Playground

解答:

ts
/*
  10 - 元组转合集
  -------
  by Anthony Fu (@antfu) #中等 #infer #tuple #union

  ### 题目

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

  例如

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

  type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'
  ``

  > 在 Github 上查看:https://tsch.js.org/10/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<TupleToUnion<[123, '456', true]>, 123 | '456' | true>>,
  Expect<Equal<TupleToUnion<[123]>, 123>>,
]

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/10/answer/zh-CN
  > 查看解答:https://tsch.js.org/10/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

可串联构造器

在 JavaScript 中我们经常会使用可串联(Chainable/Pipeline)的函数构造一个对象,但在 TypeScript 中,你能合理的给它赋上类型吗?

在这个挑战中,你可以使用任意你喜欢的方式实现这个类型 - Interface, Type 或 Class 都行。你需要提供两个函数 option(key, value)get()。在 option 中你需要使用提供的 key 和 value 扩展当前的对象类型,通过 get 获取最终结果。

例如:

ts
declare const config: Chainable

const result = config
  .option('foo', 123)
  .option('name', 'type-challenges')
  .option('bar', { value: 'Hello World' })
  .get()

// 期望 result 的类型是:
interface Result {
  foo: number
  name: string
  bar: {
    value: string
  }
}

Playground

解答:

ts
/*
  12 - 可串联构造器
  -------
  by Anthony Fu (@antfu) #中等 #application

  ### 题目

  在 JavaScript 中我们经常会使用可串联(Chainable/Pipeline)的函数构造一个对象,但在 TypeScript 中,你能合理的给它赋上类型吗?

  在这个挑战中,你可以使用任意你喜欢的方式实现这个类型 - Interface, Type 或 Class 都行。你需要提供两个函数 `option(key, value)` 和 `get()`。在 `option` 中你需要使用提供的 key 和 value 扩展当前的对象类型,通过 `get` 获取最终结果。

  例如

  ``ts
  declare const config: Chainable

  const result = config
    .option('foo', 123)
    .option('name', 'type-challenges')
    .option('bar', { value: 'Hello World' })
    .get()

  // 期望 result 的类型是:
  interface Result {
    foo: number
    name: string
    bar: {
      value: string
    }
  }
  ``

  你只需要在类型层面实现这个功能 - 不需要实现任何 TS/JS 的实际逻辑。

  你可以假设 `key` 只接受字符串而 `value` 接受任何类型,你只需要暴露它传递的类型而不需要进行任何处理。同样的 `key` 只会被使用一次。

  > 在 Github 上查看:https://tsch.js.org/12/zh-CN
*/

/* _____________ 你的代码 _____________ */

// 1. 可以使用 T = {} 来作为默认值,甚至默认参数与默认返回值,再通过递归传递 T 即可实现递归全局记录
// 2. option 是一个函数接收两个值:K 和 V,为了约束 key 不可重复必须范型传入,value 是任意类型范型不做约束直接透传
// 3. 验证重复 key,实现传入相同 key 报错
// 4. 直接 & 联合并不能将相同 key 的类型覆盖,因此用 Omit 去掉前一个类型中相同的 key
type Chainable<T = {}> = {
  option<K extends string, V>(key: K extends keyof T ? never : K, value: V): Chainable<Omit<T, K> & Record<K, V>>
  get(): T
}

/* _____________ 测试用例 _____________ */
import type { Alike, Expect } from '@type-challenges/utils'

declare const a: Chainable

const result1 = a
  .option('foo', 123)
  .option('bar', { value: 'Hello World' })
  .option('name', 'type-challenges')
  .get()

const result2 = a
  .option('name', 'another name')
  // @ts-expect-error
  .option('name', 'last name')
  .get()

const result3 = a
  .option('name', 'another name')
  // @ts-expect-error
  .option('name', 123)
  .get()

type cases = [
  Expect<Alike<typeof result1, Expected1>>,
  Expect<Alike<typeof result2, Expected2>>,
  Expect<Alike<typeof result3, Expected3>>,
]

type Expected1 = {
  foo: number
  bar: {
    value: string
  }
  name: string
}

type Expected2 = {
  name: string
}

type Expected3 = {
  name: number
}

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/12/answer/zh-CN
  > 查看解答:https://tsch.js.org/12/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

最后一个元素

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

例如:

ts
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

Playground

解答:

ts
/*
  15 - 最后一个元素
  -------
  by Anthony Fu (@antfu) #中等 #array

  ### 题目

  > 由谷歌自动翻译,欢迎 PR 改进翻译质量。

  >在此挑战中建议使用TypeScript 4.0

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

  例如

  ``ts
  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
  ``

  > 在 Github 上查看:https://tsch.js.org/15/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Last<[3, 2, 1]>, 1>>,
  Expect<Equal<Last<[() => 123, { a: string }]>, { a: string }>>,
]

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/15/answer/zh-CN
  > 查看解答:https://tsch.js.org/15/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

出堆

实现一个通用 Pop<T>,它接受一个数组 T,并返回一个由数组 T 的前 length-1 项以相同的顺序组成的数组。

例如:

ts
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]

Playground

解答:

ts
/*
  16 - 出堆
  -------
  by Anthony Fu (@antfu) #中等 #array

  ### 题目

  >在此挑战中建议使用TypeScript 4.0

  实现一个通用`Pop<T>`,它接受一个数组`T`,并返回一个由数组`T`的前length-1项以相同的顺序组成的数组。

  例如

  ``ts
  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]
  ``

  **额外**:同样,您也可以实现`Shift`,`Push`和`Unshift`吗?

  > 在 Github 上查看:https://tsch.js.org/16/zh-CN
*/

/* _____________ 你的代码 _____________ */

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

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Pop<[3, 2, 1]>, [3, 2]>>,
  Expect<Equal<Pop<['a', 'b', 'c', 'd']>, ['a', 'b', 'c']>>,
  Expect<Equal<Pop<[]>, []>>,
]

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/16/answer/zh-CN
  > 查看解答:https://tsch.js.org/16/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

Promise.all

Playground

ts
/*
  20 - Promise.all
  -------
  by Anthony Fu (@antfu) #中等 #array #promise

  ### 题目

  给函数`PromiseAll`指定类型,它接受元素为 Promise 或者类似 Promise 的对象的数组,返回值应为`Promise<T>`,其中`T`是这些 Promise 的结果组成的数组。

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

  // 应推导出 `Promise<[number, 42, string]>`
  const p = PromiseAll([promise1, promise2, promise3] as const)
  ``

  > 在 Github 上查看:https://tsch.js.org/20/zh-CN
*/

/* _____________ 你的代码 _____________ */

declare function PromiseAll<T extends readonly any[]>(
	values: readonly [...T],
): Promise<{
	[K in keyof T]: Awaited<T[K]>;
}>;

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

const promiseAllTest1 = PromiseAll([1, 2, 3] as const)
const promiseAllTest2 = PromiseAll([1, 2, Promise.resolve(3)] as const)
const promiseAllTest3 = PromiseAll([1, 2, Promise.resolve(3)])
const promiseAllTest4 = PromiseAll<Array<number | Promise<number>>>([1, 2, 3])

type cases = [
  Expect<Equal<typeof promiseAllTest1, Promise<[1, 2, 3]>>>,
  Expect<Equal<typeof promiseAllTest2, Promise<[1, 2, number]>>>,
  Expect<Equal<typeof promiseAllTest3, Promise<[number, number, number]>>>,
  Expect<Equal<typeof promiseAllTest4, Promise<number[]>>>,
]

/* _____________ 下一步 _____________ */
/*
  > 分享你的解答:https://tsch.js.org/20/answer/zh-CN
  > 查看解答:https://tsch.js.org/20/solutions
  > 更多题目:https://tsch.js.org/zh-CN
*/

Type Lookup

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

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

例如:

ts
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`

Playground

解答:

ts
// ============= Test Cases =============
import type { Equal, Expect } from './test-utils';

interface Cat {
	type: 'cat';
	breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal';
}

interface Dog {
	type: 'dog';
	breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer';
	color: 'brown' | 'white' | 'black';
}

type Animal = Cat | Dog;

type cases = [Expect<Equal<LookUp<Animal, 'dog'>, Dog>>, Expect<Equal<LookUp<Animal, 'cat'>, Cat>>];

// ============= Your Code Here =============
type LookUp<U, T> = U extends { type: T } ? U : never;

Trim Left

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

例如:

ts
type trimed = TrimLeft<'  Hello World  '> // 应推导出 'Hello World  '

Playground

解答:

ts
// ============= Test Cases =============
import type { Equal, Expect } from './test-utils';

type cases = [
	Expect<Equal<TrimLeft<'str'>, 'str'>>,
	Expect<Equal<TrimLeft<' str'>, 'str'>>,
	Expect<Equal<TrimLeft<'     str'>, 'str'>>,
	Expect<Equal<TrimLeft<'     str     '>, 'str     '>>,
	Expect<Equal<TrimLeft<'   \n\t foo bar '>, 'foo bar '>>,
	Expect<Equal<TrimLeft<''>, ''>>,
	Expect<Equal<TrimLeft<' \n\t'>, ''>>,
];

// ============= Your Code Here =============
type Space = ' ' | '\n' | '\t';
type TrimLeft<S extends string> = S extends `${Space}${infer Rest}` ? TrimLeft<Rest> : S;

Trim

实现 Trim<T>,它是一个字符串类型,并返回一个新字符串,其中两端的空白符都已被删除。

例如:

ts
type trimed = Trim<'  Hello World  '> // expected to be 'Hello World'

Playground

解答:

ts
// ============= Test Cases =============
import type { Equal, Expect } from './test-utils';

type cases = [
	Expect<Equal<Trim<'str'>, 'str'>>,
	Expect<Equal<Trim<' str'>, 'str'>>,
	Expect<Equal<Trim<'     str'>, 'str'>>,
	Expect<Equal<Trim<'str   '>, 'str'>>,
	Expect<Equal<Trim<'     str     '>, 'str'>>,
	Expect<Equal<Trim<'   \n\t foo bar \t'>, 'foo bar'>>,
	Expect<Equal<Trim<''>, ''>>,
	Expect<Equal<Trim<' \n\t '>, ''>>,
];

// ============= Your Code Here =============
type Space = ' ' | '\t' | '\n';
type Trim<S extends string> = S extends `${Space}${infer R}` | `${infer R}${Space}` ? Trim<R> : S;

Capitalize

实现 Capitalize<T>,它将字符串的首字母转换为大写。

例如:

ts
type capitalized = Capitalize<'hello world'> // expected to be 'Hello world'

Playground

解答:

ts
/* _____________ 你的代码 _____________ */

type MyCapitalize<S extends string> = S extends `${infer F}${infer R}` ? `${Uppercase<F>}${R}` : S;

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<MyCapitalize<'foobar'>, 'Foobar'>>,
  Expect<Equal<MyCapitalize<'FOOBAR'>, 'FOOBAR'>>,
  Expect<Equal<MyCapitalize<'foo bar'>, 'Foo bar'>>,
  Expect<Equal<MyCapitalize<''>, ''>>,
  Expect<Equal<MyCapitalize<'a'>, 'A'>>,
  Expect<Equal<MyCapitalize<'b'>, 'B'>>,
  Expect<Equal<MyCapitalize<'c'>, 'C'>>,
  Expect<Equal<MyCapitalize<'d'>, 'D'>>,
  Expect<Equal<MyCapitalize<'e'>, 'E'>>,
  Expect<Equal<MyCapitalize<'f'>, 'F'>>,
  Expect<Equal<MyCapitalize<'g'>, 'G'>>,
  Expect<Equal<MyCapitalize<'h'>, 'H'>>,
  Expect<Equal<MyCapitalize<'i'>, 'I'>>,
  Expect<Equal<MyCapitalize<'j'>, 'J'>>,
  Expect<Equal<MyCapitalize<'k'>, 'K'>>,
  Expect<Equal<MyCapitalize<'l'>, 'L'>>,
  Expect<Equal<MyCapitalize<'m'>, 'M'>>,
  Expect<Equal<MyCapitalize<'n'>, 'N'>>,
  Expect<Equal<MyCapitalize<'o'>, 'O'>>,
  Expect<Equal<MyCapitalize<'p'>, 'P'>>,
  Expect<Equal<MyCapitalize<'q'>, 'Q'>>,
  Expect<Equal<MyCapitalize<'r'>, 'R'>>,
  Expect<Equal<MyCapitalize<'s'>, 'S'>>,
  Expect<Equal<MyCapitalize<'t'>, 'T'>>,
  Expect<Equal<MyCapitalize<'u'>, 'U'>>,
  Expect<Equal<MyCapitalize<'v'>, 'V'>>,
  Expect<Equal<MyCapitalize<'w'>, 'W'>>,
  Expect<Equal<MyCapitalize<'x'>, 'X'>>,
  Expect<Equal<MyCapitalize<'y'>, 'Y'>>,
  Expect<Equal<MyCapitalize<'z'>, 'Z'>>,
]

Replace

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

例如:

ts
type replaced = Replace<'types are fun!', 'fun', 'awesome'> // 期望是 'types are awesome!'

Playground

解答:

ts
/* _____________ 你的代码 _____________ */

type Replace<S extends string, From extends string, To extends string> = From extends '' ? S : S extends `${infer L}${From}${infer R}` ? `${L}${To}${R}` : S

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Replace<'foobar', 'bar', 'foo'>, 'foofoo'>>,
  Expect<Equal<Replace<'foobarbar', 'bar', 'foo'>, 'foofoobar'>>,
  Expect<Equal<Replace<'foobarbar', '', 'foo'>, 'foobarbar'>>,
  Expect<Equal<Replace<'foobarbar', 'bar', ''>, 'foobar'>>,
  Expect<Equal<Replace<'foobarbar', 'bra', 'foo'>, 'foobarbar'>>,
  Expect<Equal<Replace<'', '', ''>, ''>>,
]

ReplaceAll

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

例如:

ts
type replaced = ReplaceAll<'t y p e s', ' ', ''> // 期望是 'types'

Playground

解答:

ts
/* _____________ 你的代码 _____________ */

type ReplaceAll<S extends string, From extends string, To extends string> = From extends '' ? S : S extends `${infer L}${From}${infer R}` ? `${L}${To}${ReplaceAll<R, From, To>}` : S

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<ReplaceAll<'foobar', 'bar', 'foo'>, 'foofoo'>>,
  Expect<Equal<ReplaceAll<'foobar', 'bag', 'foo'>, 'foobar'>>,
  Expect<Equal<ReplaceAll<'foobarbar', 'bar', 'foo'>, 'foofoofoo'>>,
  Expect<Equal<ReplaceAll<'t y p e s', ' ', ''>, 'types'>>,
  Expect<Equal<ReplaceAll<'foobarbar', '', 'foo'>, 'foobarbar'>>,
  Expect<Equal<ReplaceAll<'barfoo', 'bar', 'foo'>, 'foofoo'>>,
  Expect<Equal<ReplaceAll<'foobarfoobar', 'ob', 'b'>, 'fobarfobar'>>,
  Expect<Equal<ReplaceAll<'foboorfoboar', 'bo', 'b'>, 'foborfobar'>>,
  Expect<Equal<ReplaceAll<'', '', ''>, ''>>,
]

Append Argument

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

示例:

ts
type Fn = (a: number, b: string) => number

type Result = AppendArgument<Fn, boolean> 
// 期望是 (a: number, b: string, x: boolean) => number

Playground

解答:

ts
// ============= Test Cases =============
import type { Equal, Expect } from './test-utils';

type Case1 = AppendArgument<(a: number, b: string) => number, boolean>;
type Result1 = (a: number, b: string, x: boolean) => number;

type Case2 = AppendArgument<() => void, undefined>;
type Result2 = (x: undefined) => void;

type cases = [
	Expect<Equal<Case1, Result1>>,
	Expect<Equal<Case2, Result2>>,
	// @ts-expect-error
	AppendArgument<unknown, undefined>,
];

// ============= Your Code Here =============
type AppendArgument<Fn extends (...args: any[]) => unknown, A> = Fn extends (
	...args: infer R
) => infer V
	? (...args: [...R, A]) => V
	: never;

Permutation

实现联合类型的全排列,将联合类型转换成所有可能的全排列数组的联合类型。

示例:

ts
type perm = Permutation<'A' | 'B' | 'C'>; 
// ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']

Playground

解答:

ts
// ============= Test Cases =============
import type { Equal, Expect } from './test-utils';

type cases = [
	Expect<Equal<Permutation<'A'>, ['A']>>,
	Expect<
		Equal<
			Permutation<'A' | 'B' | 'C'>,
			| ['A', 'B', 'C']
			| ['A', 'C', 'B']
			| ['B', 'A', 'C']
			| ['B', 'C', 'A']
			| ['C', 'A', 'B']
			| ['C', 'B', 'A']
		>
	>,
	Expect<
		Equal<
			Permutation<'B' | 'A' | 'C'>,
			| ['A', 'B', 'C']
			| ['A', 'C', 'B']
			| ['B', 'A', 'C']
			| ['B', 'C', 'A']
			| ['C', 'A', 'B']
			| ['C', 'B', 'A']
		>
	>,
	Expect<Equal<Permutation<boolean>, [false, true] | [true, false]>>,
	Expect<Equal<Permutation<never>, []>>,
];

// ============= Your Code Here =============
// 方案一:
// type Permutation<T, K = T> = [T] extends [never]
// 	? []
// 	: T extends infer P
// 	? [P, ...Permutation<Exclude<K, P>>]
// 	: [];

// 方案二:
type Permutation<T, K = T> = [T] extends [never]
	? []
	: K extends K
	? [K, ...Permutation<Exclude<T, K>>]
	: [];

Length of String

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

Playground

解答:

ts
// ============= Test Cases =============
import type { Equal, Expect } from './test-utils';

type cases = [
	Expect<Equal<LengthOfString<''>, 0>>,
	Expect<Equal<LengthOfString<'kumiko'>, 6>>,
	Expect<Equal<LengthOfString<'reina'>, 5>>,
	Expect<Equal<LengthOfString<'Sound! Euphonium'>, 16>>,
];

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