小镇做题家 - TypeScript 类型大挑战(中等篇 - 上)
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 argumentT
andK
.
K
specify the set of properties ofT
that should set to Readonly. WhenK
is not provided, it should make all properties readonly just like the normalReadonly<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)
andget()
. Inoption
, you can extend the current config type by the given key and value. We should about to access the final result viaget
.
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 ArrayT
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 ArrayT
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 bePromise<T>
whereT
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 unionCat | Dog
. In other words, we will expect to getDog
forLookUp<Dog | Cat, 'dog'>
andCat
forLookUp<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 stringFrom
withTo
once in the given stringS
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 substringFrom
withTo
in the given stringS
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 typeA
(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 takeFn
as the first argument,A
as the second, and will produce function typeG
which will be the same asFn
but with appended argumentA
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
}