小镇做题家 - TypeScript 类型大挑战(中等篇 - 上)

前端开发
2022年09月25日
788

Medium 组(上)

Get Return Type

Implement the built-in ReturnType<T> generic without using it.

typescript
type MyReturnType<T> = any /* _____________ Test Cases _____________ */ 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

啊哈,我们刚完成了对函数数据的提取,取返回值的思路与之类似:

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

Omit

Implement the built-in Omit<T, K> generic without using it.

typescript
type MyOmit<T, K> = any /* _____________ Test Cases _____________ */ 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 }

Omit 是一个内置的工具类型,它的官方解释是:通过从 Type 中选择所有属性然后删除 Keys(字符串或字符串组成的联合类型)来构造一个类型。

我们先看 //@ts-expect-error,很显然,K 必须是 T 里面的键才符合要求:

typescript
type MyOmit<T, K extends keyof T> = any

根据我们之前解决 Pick 的思路解决即可:

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

Readonly

Implement a generic MyReadonly2<T, K> which takes two type argument T and K.

K specify the set of properties of T that should set to Readonly. When K is not provided, it should make all properties readonly just like the normal Readonly<T>.

typescript
type MyReadonly2<T, K> = any /* _____________ Test Cases _____________ */ 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>>, ] // @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 }

这题和 Pick 的解题思路差不多,但需要注意的是区分 readonly 项和普通项,所以这里我们采用交叉类型来完成这两个部分:

typescript
type MyReadonly2<T, K extends keyof T = keyof T> = { readonly [P in keyof T as P extends K ? P : never]: T[P] } & { [P in keyof T as P extends K ? never : P]: T[P] }

Deep Readonly

Implement a generic DeepReadonly<T> which make every parameter of an object - and its sub-objects recursively - readonly.

You can assume that we are only dealing with Objects in this challenge. Arrays, Functions, Classes and so on do not need to be taken into consideration. However, you can still challenge yourself by covering as many different cases as possible.

typescript
type DeepReadonly<T> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<DeepReadonly<X>, Expected>>, ] type X = { a: () => 22 b: string c: { d: boolean e: { g: { h: { i: true j: 'string' } k: 'hello' } l: [ 'hi', { m: ['hey'] }, ] } } } type Expected = { 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'] }, ] } } }

一看到 Deep,我们就知道应该使用递归了,这里递归的条件是:非函数类型的对象类型:

typescript
type IsObject<T> = T extends Record<string, any> ? T extends Function ? false : true : false // 或者 type IsObject<T> = T extends object ? T extends Function ? false : true : false

然后我们就可以递归操作了:

typescript
type DeepReadonly<T> = { readonly [P in keyof T]: IsObject<T[P]> extends true ? DeepReadonly<T[P]> : T[P] }

Tuple To Union

Implement a generic TupleToUnion<T> which covers the values of a tuple to its values union.

typescript
type TupleToUnion<T> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<TupleToUnion<[123, '456', true]>, 123 | '456' | true>>, Expect<Equal<TupleToUnion<[123]>, 123>>, ]

这个我们在解 Exclude 时就了解了分布式条件类型,我们只需要把 T 限制为 Array 类型,通过 T[number] 即可解决:

typescript
type TupleToUnion<T extends unknown[]> = T[number]

ChainableOptions

Chainable options are commonly used in Javascript. But when we switch to TypeScript, can you properly type it?

In this challenge, you need to type an object or a class - whatever you like - to provide two function option(key, value) and get(). In option, you can extend the current config type by the given key and value. We should about to access the final result via get.

typescript
type Chainable = { option(key: string, value: any): any get(): any } /* _____________ Test Cases _____________ */ 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') .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 }

在解题之前,我们先看看 JavaScript 中的链式调用是怎么设计的:

js
const a = { options (name, value) { this[name] = value return this }, get (name) { return this[name] } } const b = a .options('foo', 'bar') .get('foo') const c = a .options('foo', 'bar') .options('foo', 'baz') .get('foo') console.log(b) // 'bar' console.log(c) // 'baz'

起到链式调用的关键是在 options() 中返回了当前对象 this

首先,我们需要一个地方来保存结果,这样我们才能在 get() 被调用时取得对应的结果:

typescript
type Chainable<R = {}> = { option(key: string, value: any): any get(): R }

然后,再让 options() 被调用时,把对应的 key 和 value 存储到结果 R 中:

typescript
type Chainable<R = {}> = { option<K extends string, V>(key: K, value: V): Chainable<{ [P in K]: V }> get(): R }

当然,我们需要保证结果 R 中不存在本次传进来的 K,还记得我们之前实现的 Omit 吗?通过 Omit 在结果 R 中排除掉可能存在的 K 即可:

typescript
type Chainable<R = {}> = { option<K extends string, V>(key: K, value: V): Chainable<Omit<R, K> & { [P in K]: V }> get(): R }

最后,我们需要解决掉 // @ts-expect-error 那个 case,可以看到,两次调用 options() 传入了相同的 key 和相同类型的 value,并且该注释是在 options() 上的,所以要在 options() 这里来处理:

typescript
type Chainable<R = {}> = { option<K extends string, V>(key: number, value: V): Chainable<Omit<R, K> & { [P in K]: V }> get(): R }

当我们尝试把 key 的类型改为一个非 string 类型时发现,// @ts-expect-error 这条注释取得了预想中的效果,后面只需要确保在 R 中不存在相同的 key 和同类型的 value 这一条件即可,所以我们可以再加一个类型,用于检测,并返回 key 最终的类型:

typescript
type GetKeyType<T, K extends string, V> = K extends keyof T ? T[K] extends V ? [] // 只要不是 string 类型即可 : K : K

然后再在把 GetKeyType 赋予 key 即可:

typescript
type GetKeyType<T, K extends string, V> = K extends keyof T ? T[K] extends V ? number // 只要不是 string 类型即可 : K : K type Chainable<R = {}> = { option<K extends string, V>(key: GetKeyType<R, K, V>, value: V): Chainable<Omit<R, K> & { [P in K]: V }> get(): R }

Last of Array

TypeScript 4.0 is recommended in this challenge

Implement a generic Last<T> that takes an Array T and returns its last element.

typescript
type Last<T extends any[]> = any /* _____________ Test Cases _____________ */ 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 }>>, ]

这题比较简单,就不细说了:

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

Pop

TypeScript 4.0 is recommended in this challenge

Implement a generic Pop<T> that takes an Array T and returns an Array without it’s last element.

typescript
type Pop<T extends any[]> = any /* _____________ Test Cases _____________ */ 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']>>, ]

这个和上面的 Last 是一样的:

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

Promise.all

Type the function PromiseAll that accepts an array of PromiseLike objects, the returning value should be Promise<T> where T is the resolved result array.

typescript
declare function PromiseAll(values: any): any /* _____________ Test Cases _____________ */ 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)]) 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]>>>, ]

可以看到,题目给出的初始代码比较简单,所以我们需要按照 case 的要求加上泛型和参数限制:

typescript
declare function PromiseAll<T extends unknown[]>(values: readonly [...T]): any

它的返回值应该是一个 Promise<any []> 类型:

typescript
declare function PromiseAll<T extends unknown[]>(values: readonly [...T]): Promise<unknown []>

众所周知,在 JavaScript 中 Array 也是一个对象,这一点在 TypeScript 中也是一样的:

typescript
type Arr1 = string[] type Arr2 = { [K: number]: string } type E = Expect<Equal<true, Arr1 extends Arr2 ? true : false>>

所以第一个 case 就很简单了:

typescript
declare function PromiseAll<T extends unknown[]>(values: readonly [...T]): Promise<{ [K in keyof T]: T[K] }>

而在第二、第三个 case 的参数数组里面存在着 Promise,所以我们要对 T[K] 作进一步的判断:

typescript
declare function PromiseAll<T extends unknown[]>(values: readonly [...T]): Promise<{ [K in keyof T]: T[K] extends Promise<infer D> ? D : T[K] }>

如此一来,所有的 cases 就解决了。

Type Lookup

Sometimes, you may want to lookup for a type in a union to by their attributes.

In this challenge, we would like to get the corresponding type by searching for the common type field in the union Cat | Dog. In other words, we will expect to get Dog for LookUp<Dog | Cat, 'dog'> and Cat for LookUp<Dog | Cat, 'cat'> in the following example.

typescript
type LookUp<U, T> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/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>>, ]

同样,我们还是根据需求把类型限制给加上:

typescript
type LookUp<U extends Animal, T extends U['type']> = any

从题中可知,无论是 Cat 还是 Dog,都是继承于 { type: string } 这个接口的,我们可以用下面的代码来测试一下:

typescript
interface Base { type: string } type A = Expect<Equal<Cat extends Base ? true : false, true>> type B = Expect<Equal<Dog extends Base ? true : false, true>>

如果我们再把 Base 里面的 type 作一下限制,那么这题就很容易解开了:

typescript
interface Base<T> { type: T } type A = Expect<Equal<Cat extends Base<'cat'> ? true : false, true>> type B = Expect<Equal<Dog extends Base<'dog'> ? true : false, true>>

所以最终我们可以得出这样的答案:

typescript
interface Base<T> { type: T } type LookUp<U extends Animal, T extends U['type']> = U extends Base<T> ? U : never

TrimLeft

Implement TrimLeft<T> which takes an exact string type and returns a new string with the whitespace beginning removed.

typescript
type TrimLeft<S extends string> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/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'>, ''>>, ]

这题考验的是字符串操作,和数组中的 Shift 很相似,我们可以使用如下代码从字符串中取值:

typescript
type A<S extends string> = S extends `${infer F}${infer R}` ? F : never type B = Expect<Equal<A<'Hello'>, 'H'>>

题目中的要求是:只要前面的字符是 ''\n 或者 \t 都不要,那么我们可以递归来完成:

typescript
type TrimLeft<S extends string> = S extends `${infer F}${infer R}` ? F extends IgnoreString ? TrimLeft<R> : S : ''

当然,我们也可以把判断放在 infer 里面:

typescript
type TrimLeft<S extends string> = S extends `${infer F extends IgnoreString}${infer R}` ? TrimLeft<R> : S

Capitalize

Implement Capitalize<T> which converts the first letter of a string to uppercase and leave the rest as-is.

typescript
type MyCapitalize<S extends string> = any /* _____________ Test Cases _____________ */ 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'>>, ]

Cases 有点多啊,不过这题也很简单,它只需要把第一个字母转成大写即可,在 TypeScript 中有个 Uppercase 的工具类可以把字母转成大写:

typescript
type A = Expect<Equal<'A', Uppercase<'a'>>> type B = Expect<Equal<'BC', Uppercase<'bc'>>>

所以,我们只需要把首字母拿出来转成大写即可:

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

Replace

Implement Replace<S, From, To> which replace the string From with To once in the given string S

typescript
type Replace<S extends string, From extends string, To extends string> = any /* _____________ Test Cases _____________ */ 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<'', '', ''>, ''>>, ]

这题同样是字符串操作,需要注意的是,如果 From 为空字符串,那就原样输出 S:

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

ReplaceAll

Implement ReplaceAll<S, From, To> which replace the all the substring From with To in the given string S

typescript
type ReplaceAll<S extends string, From extends string, To extends string> = any /* _____________ Test Cases _____________ */ 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<'', '', ''>, ''>>, ]

按照 Replace 的思路递归即可:

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

一气呵成,不愧是你,但是我们会发现,有两个 cases 并没有被解决掉:

typescript
Expect<Equal<ReplaceAll<'foobarfoobar', 'ob', 'b'>, 'fobarfobar'>> Expect<Equal<ReplaceAll<'foboorfoboar', 'bo', 'b'>, 'foborfobar'>>

我们拿 ReplaceAll<'foobarfoobar', 'ob', 'b'> 这个来说,在第一次执行替换时:

typescript
F 为 'fo' From'ob' R 为 'arfoobar'

所以我们在给第二次执行 ReplaceAll 拼接的是:${F}${To}${R}fobarfoobar,此时:

typescript
F 为 'f' From'ob' R 为 'arfoobar'

在第三次执行 ReplaceAll 拼接的是:${F}${To}${R}fbarfoobar

经过一次又一次的递归,最终得到的是 fbarfbar。这显然不符合要求中的:fobarfobar。为什么会出现这个问题?是因为我们把已经替换过后的字符再次放进了下一次递归的参数 T 中,这显然是不合理的。所以我们需要把每一次替换的结果给存储起来,增加一个泛型 Result,它的初始值为空字符串:

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

在每一次执行替换时,把结果收集起来,在下一次递归传递给 S 的值是剩余字符,最后返回 Result 和最后一次递归传入的 S 拼接起来的字符串即可:

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

Append Argument

For given function type Fn, and any type A (any in this context means we don’t restrict the type, and I don’t have in mind any type) create a generic type which will take Fn as the first argument, A as the second, and will produce function type G which will be the same as Fn but with appended argument A as a last one.

typescript
type AppendArgument<Fn, A> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/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>>, ]

我们知道函数的参数可以用 ... 来收集剩余参数,它是一个数组,而在 TypeScript 中也是如此:

typescript
type GetArgs<Fn> = Fn extends (...args: infer Args) => any ? Args : never type A = Expect<Equal<GetArgs<(a: number, b: string, c: boolean) => 1>, [number, string, boolean]>>

所以这题和 Push 是非常相似了,只不过它操作的地方是函数的参数而已:

typescript
type AppendArgument< Fn, A > = Fn extends (...args: infer Args) => infer R ? (...args: [...Args, A]) => R : never

Permutation

Implement permutation type that transforms union types into the array that includes permutations of unions.

typescript
type Permutation<T> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/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>, []>>, ]

很典型的分布式条件类型,需要注意的是,在下一次递归时要排除掉本次的值:

typescript
type Permutation<T, A = T> = [T] extends [never] ? [] : T extends A ? [T, ...Permutation<Exclude<A, T>>] : never

Length of String

Compute the length of a string literal, which behaves like String#length.

typescript
type LengthOfString<S extends string> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<LengthOfString<''>, 0>>, Expect<Equal<LengthOfString<'kumiko'>, 6>>, Expect<Equal<LengthOfString<'reina'>, 5>>, Expect<Equal<LengthOfString<'Sound! Euphonium'>, 16>>, ]

So easy,S['length'] 收工:

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

很遗憾,并没有取得预想中的效果,字符串没有 length,那只能采取迂回战术,既然字符串没有,那么我们就使用 Array:

typescript
type LengthOfString<S extends string, Arr extends number[] = []> = S extends `${infer F}${infer R}` ? LengthOfString<R, [...Arr, 0]> : Arr['length']

Flatten

In this challenge, you would need to write a type that takes an array and emitted the flatten array type.

typescript
type Flatten = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<Flatten<[]>, []>>, Expect<Equal<Flatten<[1, 2, 3, 4]>, [1, 2, 3, 4]>>, Expect<Equal<Flatten<[1, [2]]>, [1, 2]>>, Expect<Equal<Flatten<[1, 2, [3, 4], [[[5]]]]>, [1, 2, 3, 4, 5]>>, Expect<Equal<Flatten<[{ foo: 'bar'; 2: 10 }, 'foobar']>, [{ foo: 'bar'; 2: 10 }, 'foobar']>>, ]

同样是递归处理数组中的每一项,如果还是数组就继续递归:

typescript
type Flatten<T extends unknown[], Result extends unknown[] = []> = T extends [infer F, ...infer R] ? F extends unknown[] ? Flatten<R, [...Result, ...Flatten<F>]> : Flatten<R, [...Result, F]> : Result

Append to object

Implement a type that adds a new field to the interface. The type takes the three arguments. The output should be an object with the new field.

typescript
type AppendToObject<T, U, V> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type test1 = { key: 'cat' value: 'green' } type testExpect1 = { key: 'cat' value: 'green' home: boolean } type test2 = { key: 'dog' | undefined value: 'white' sun: true } type testExpect2 = { key: 'dog' | undefined value: 'white' sun: true home: 1 } type test3 = { key: 'cow' value: 'yellow' sun: false } type testExpect3 = { key: 'cow' value: 'yellow' sun: false isMotherRussia: false | undefined } type cases = [ Expect<Equal<AppendToObject<test1, 'home', boolean>, testExpect1>>, Expect<Equal<AppendToObject<test2, 'home', 1>, testExpect2>>, Expect<Equal<AppendToObject<test3, 'isMotherRussia', false | undefined>, testExpect3>>, ]

这题我们需要注意的是,U 是一个字符串,它将作为返回类型的键。

我们知道,keyof 会返回一个由对象的键组成的联合类型,此时,再把 U 给加到这个联合类型组成新的对象的键,即可解题:

typescript
type AppendToObject<T, U extends string, V> = { [P in keyof T | U]: P extends keyof T ? T[P] : V }

Absolute

Implement the Absolute type. A type that take string, number or bigint. The output should be a positive number string

typescript
type Absolute<T extends number | string | bigint> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<Absolute<0>, '0'>>, Expect<Equal<Absolute<-0>, '0'>>, Expect<Equal<Absolute<10>, '10'>>, Expect<Equal<Absolute<-5>, '5'>>, Expect<Equal<Absolute<'0'>, '0'>>, Expect<Equal<Absolute<'-0'>, '0'>>, Expect<Equal<Absolute<'10'>, '10'>>, Expect<Equal<Absolute<'-5'>, '5'>>, Expect<Equal<Absolute<-1_000_000n>, '1000000'>>, Expect<Equal<Absolute<9_999n>, '9999'>>, ]

在 JavaScript 中取绝对值时可以使用 Math.abs(),但在 TypeScript 中就得需要一些其他操作了:

typescript
type Absolute<T extends number | string | bigint> = `${T}` extends `-${infer R}` ? R : `${T}`

同样是字符串操作,把 - 号移除即可。

String to Union

Implement the String to Union type. Type take string argument. The output should be a union of input letters

typescript
type StringToUnion<T extends string> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<StringToUnion<''>, never>>, Expect<Equal<StringToUnion<'t'>, 't'>>, Expect<Equal<StringToUnion<'hello'>, 'h' | 'e' | 'l' | 'l' | 'o'>>, Expect<Equal<StringToUnion<'coronavirus'>, 'c' | 'o' | 'r' | 'o' | 'n' | 'a' | 'v' | 'i' | 'r' | 'u' | 's'>>, ]

字符串递归即可,提供一个 Result 来收集每次递归的结果,初始值为 never:

typescript
type StringToUnion<T extends string, Result = never> = T extends `${infer F}${infer R}` ? StringToUnion<R, Result | F> : Result

Merge

Merge two types into a new type. Keys of the second type overrides keys of the first type.

typescript
type Merge<F, S> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type Foo = { a: number b: string } type Bar = { b: number c: boolean } type cases = [ Expect<Equal<Merge<Foo, Bar>, { a: number b: number c: boolean }>>, ]

这一题和前面做的 [Append to object](#Append to object) 很相似:

typescript
type Merge<F, S> = { [P in keyof F | keyof S]: P extends keyof S ? S[P] : P extends keyof F ? F[P] : never }