Skip to content

类型体操-中等

更加疯狂了。

获取函数返回类型 中等 #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"

答案:

ts
// 最初的问题在于没有考虑到函数的参数的问题,所以面对具有参数的函数这个类型就会失效
type MyReturnType<T extends () => any> = T extends () => (infer U) ? U : never

// 参考答案后将函数参数补上
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => (infer U) ? U : never

// 标准答案,使用了Function类型
type MyReturnType<T extends Function> = T extends (...args: any) => infer R ? R : never

实现 Omit 中等 #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,
}

答案:

ts
// kt in keyof T as kt extends K 将同时是T的键与K的值的字段选出,如果值存在返回never,否则返回ky,这样就得到了所需的所有键,最后将键与值组合。它的效果和Pick泛型正好是相反的。
type MyOmit<T, K extends keyof T> = {
	[kt in keyof T as kt extends K ? never: kt] :T[kt]
}

对象部分属性只读 中等 #readonly #object-keys

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

类型 K 指定 T 中要被设置为只读 (readonly) 的属性。如果未提供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

答案:

ts
// 可以使用Omit与Pick泛型来实现,不过这里既然要追求刺激那就要贯彻到底不是吗?
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] }

对象属性只读(递归) 中等 #readonly #object-keys #deep

实现一个泛型 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`

答案:

ts
type DeepReadonly<T> =keyof T extends never 
  ? T 
  : { readonly [k in keyof T]: DeepReadonly<T[k]> };

元组转联合 中等 #infer #tuple #union

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

例如:

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

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

答案:

ts
// 两种不同的实现
type TupleToUnion<T extends any[]> = T[number]

type TupleToUnion<T> = T extends Array<infer U> ? U : never

可串联构造器 中等 #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

最后一个元素 中等 #array

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

例如:

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

type tail1 = Last<arr1> // 应推导出 'c'
type tail2 = Last<arr2> // 应推导出 1

答案:

ts
// 这里使用...扩展数组的元素,并用[T["length"]]来获取到其最后一个元素
type Last<T extends any[]> = [...T][T["length"]]

排除最后一项 中等 #array

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

例如:

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]

答案:

ts
type Pop<T extends any[]> = T extends [...infer I, infer _] ? I : never

Promise.all 中等 #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)

答案:

ts
declare function PromiseAll<T extends any[]>(values: T ):
	Promise<{[key in keyof T] : T[key] extends Promise<infer R> ? R : T[key] }>

查找类型 中等 #union #map

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

在此挑战中,我们想通过在联合类型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`

答案:

ts
type LookUp<U, T> = U extends T ? U : never

去除左侧空白 中等 #template-literal

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

例如:

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

答案:

ts
type TrimLeft<S extends string> = 
	S extends `${ ' ' | '\n' | '\t' }${infer R}` ? TrimLeft<R> : S

去除两端空白字符 中等 #template-literal

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

例如:

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

答案:

ts
// 我的实现
type Trim<S extends string> = 
	S extends `${' ' | '\n' | '\t'}${infer T}${' ' | '\n' | '\t'}` 
	? Trim<T> : S

// 答案
type Space = ' ' | '\t' | '\n';
type Trim<S extends string> = 
	S extends `${Space}${infer T}` | `${infer T}${Space}` ? Trim<T> : S;

Capitalize 中等 #template-literal

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

例如:

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

答案:

TypeScript 在使用 infer 进行字符串解构时,会尽可能少地匹配内容(即“非贪婪”),以使整个模式能成功匹配,所以Firsth不是he或者hel

ts
type MyCapitalize<S extends string> = 
	S extends `${infer First}${infer Rest}` 
	? `${Uppercase<First>}${Rest}` : S

Replace 中等 #template-literal

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

例如:

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

答案:

ts
// 这个答案有一个问题,不能处理From是''的情况
type Replace<S extends string, From extends string, To extends string> = 
	S extends `${infer F}${From}${infer B}` 
		? `${F}${To}${B}` 
		: S
/*
if (S extends `${infer F}${From}${infer B}`) {
	return `${F}${To}${B}`
} else {
	return S
}
**/

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

ReplaceAll 中等 #template-literal

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

例如:

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

答案:

ts
// 这个没有办法处理String一开始不是From的
type ReplaceAll<S extends string, From extends string, To extends string> = 
	S extends `${From}${infer Rest}` 
	? `${To}${ReplaceAll<Rest,From,To>}` 
	: S
	
// 没有考虑到''
type ReplaceAll<S extends string, From extends string, To extends string> = 
	S extends `${infer F}${From}${infer Rest}` 
	? `${F}${To}${ReplaceAll<Rest,From,To>}` 
	: S
	
// 答案
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>}`
  : S

追加参数 中等 #arguments

> 由 @antfu 翻译

实现一个泛型 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

答案:

ts
type AppendArgument<Fn extends Function, A> = 
	// `...args` 在这里是运行时的展开,将参数收集到T元组中
	Fn extends (...args: infer T) => infer R 
	// `...args` 在这里是类型声明语法,不是运行时的展开。`...T`,将T元组展开。
	? (...args: [...T, A]) => R 
	: never

Permutation 中等 #union

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

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

答案: 这题肯定不是中等好吧

Length of String 中等 #template-literal

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

答案:我们想要得到一个字符串T的长度是不能直接T['length']的,这会返回number,不是一个具体的数字,而一个数组却可以

ts
// 借助其他的泛型,将字符串转化为数组
type StringToArray<S extends string> = 
	S extends `${infer F}${infer R}` 
	? [F, ...StringToArray<R>] 
	: []

type LengthOfString<S extends string> = StringToArray<S>['length']

Flatten 中等 #array

在这个挑战中,你需要写一个接受数组的类型,并且返回扁平化的数组类型。

例如:

ts
type flatten = Flatten<[1, 2, [3, 4], [[5]([5.md)]]> // [1, 2, 3, 4, 5]

答案:

ts
type Flatten<T extends any[]> =
	T extends [infer A, ...infer B]
	? A extends any[]
		? [...Flatten<A>, ...Flatten<B>]
		: [A, ...Flatten<B>]
	: []

Append to object 中等 #object-keys

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

例如:

ts
type Test = { id: '1' }
type Result = AppendToObject<Test, 'value', 4> // expected to be { id: '1', value: 4 }

答案:

ts
// 太妙了
type AppendToObject<T, U extends keyof any, V> = {
	[key in keyof T | U] : key extends keyof T 
	? T[key] 
	: V
}

Absolute 中等 #math #template-literal

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

例如

ts
type Test = -100;
type Result = Absolute<Test>; // expected to be "100"

答案:

ts
// 实际上根本就不用这样写
type AbsoluteString<T extends string> =
	T extends `-${infer Rest}`
	? Rest
	: T

type AbsoluteNumber<T extends number> = any

type AbsoluteBigint<T extends bigint> = any

type Absolute<T extends string | number | bigint> =
	T extends string
	? AbsoluteString<T>
	  : T extends number
	    ? AbsoluteNumber<T>
	    : T extends bigint
	      ? AbsoluteBigint<T>
	      : never

// 这样就解决了
type Absolute<T extends number | string | bigint> = 
	`${T}` extends `-${infer Rest}` 
	? Rest 
	: `${T}`

String to Union 中等 #union #string

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

例如

ts
type Test = '123';
type Result = StringToUnion<Test>; // expected to be "1" | "2" | "3"

答案:

ts
// 终于做出来一道,好爽啊
type StringToArray<T extends string> = 
	T extends `${infer F}${infer Rest}` 
	? [F, ...StringToArray<Rest>] 
	: []

// 数组是元组的一种特例,所以这个泛型也可处理数组
type TupleToUnion<T extends any[]> = T[number]

// 结合已经实现的两个泛型
type StringToUnion<T extends string> = TupleToUnion<StringToArray<T>>

Merge 中等 #object

例如:

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

答案:

ts
// 整体的思路是对的,但是实现的过程有些问题
type Merge<F, S> = {
	[key in keyof (F | S)] : key extends S 
	? S[key] 
	: F[key] 
}

// 答案
type Merge<F, S> = {
	[key in keyof F | keyof S] : key extends keyof S 
	? S[key] 
	: key extends keyof F 
		? F[key] 
		: never
}

KebabCase medium #template-literal

Replace the camelCase or PascalCase string with kebab-case.

FooBarBaz -> foo-bar-baz

For example

ts
type FooBarBaz = KebabCase<"FooBarBaz">
const foobarbaz: FooBarBaz = "foo-bar-baz"

type DoNothing = KebabCase<"do-nothing">
const doNothing: DoNothing = "do-nothing"

答案: First中的为第一个字符,Rest为剩余的字符,如果Rest是小写开头的,那么就不加-,反之

ts
type KebabCase<S extends string> = S extends `${infer First}${infer Rest}` 
	? Rest extends Uncapitalize<Rest> 
		? `${Uncapitalize<First>}${KebabCase<Rest>}` 
		: `${Uncapitalize<First>}-${KebabCase<Rest>}` 
	: S

Diff 中等 #object

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

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

答案:

ts
// 这种写法就是实现了一个Omit,既不优雅也不能实现
// type Diff<O, O1> = {[K in keyof O | keyof O1] : K extends keyof O & keyof O1 ? never : K extends keyof O ? O[K] : K extends keyof O1 ? O1[K] : never}

type Diff<O, O1> = Omit<O & O1, keyof (O | O1)>

在对象中使用 | 与 &,与在非对象中使用存在语义上的差异。

在集合对象中使用联合类型 | ,官网 working-with-union-types 有如下说明:

> Notice that given two sets with corresponding facts about each set, only the intersection of those facts applies to the union of the sets themselves.

ts
type Foo = {
  name: string
  age: string
}
type Bar = {
  name: string
  age: string
  gender: number
}

type result = keyof (Foo | Bar) // "name" | "age"

在集合对象中使用交集类型 & ,可以见 intersection-types 给出的 demo:

ts
interface Colorful {
  color: string;
}
interface Circle {
  radius: number;
}

type ColorfulCircle = keyof (Colorful & Circle) // "color" | "radius"

结合 & 与 | 的使用,我们能立马写出比如类型 diff

AnyOf 中等 #array

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

例如:

ts
type Sample1 = AnyOf<[1, '', false, [], {}]> // expected to be true.
type Sample2 = AnyOf<[0, '', false, [], {}]> // expected to be false.

答案:

ts
type AnyOf<T extends any[]> =
T[number] extends 0 | '' | false | [] | {[key: string]: never}
? false
: true;

IsNever medium #union #utils

Implement a type IsNever, which takes input type T. If the type of resolves to never, return true, otherwise false.

For example:

ts
type A = IsNever<never> // expected to be true
type B = IsNever<undefined> // expected to be false
type C = IsNever<null> // expected to be false
type D = IsNever<[]> // expected to be false
type E = IsNever<number> // expected to be false

答案:

ts
// 这里有一些小问题,如果T是never,那么返回的类型就是never,这有点奇怪
type IsNever<T> = T extends never ? true : false

type IsNever<T> = [T] extends [never] ? true : false

所以这里的[T] extends [never]是什么?

IsNever<never> 中的 never 实际上是一个空的联合类型,一项都没有,所以 T extends ... 过程实际上被整体跳过了,所以最后的结果就是 never。[中文翻译](TS 类型体操笔记 - 296 Permutation类型体操 是关于 TS 类型编程的一系列挑战(编程题目)。 本文是 - 掘金)

IsUnion medium #union

Implement a type IsUnion, which takes an input type T and returns whether T resolves to a union type.

For example:

ts
type case1 = IsUnion<string> // false
type case2 = IsUnion<string | number> // true
type case3 = IsUnion<[string | number]> // false