1

The best way to solve the TS problem is to practice more. This time, I will interpret the difficulty of type-challenges Medium questions 17~24.

intensive reading

Permutation

Implement the Permutation type, replacing the union type with all possible permutations:

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

When I saw this question, I immediately thought that TS uses the distributive law for the generic processing of multiple union types. I encountered it when I did it for the first time Exclude the title:

 Exclude<'a' | 'b', 'a' | 'c'>
// 等价于
Exclude<'a', 'a' | 'c'> | Exclude<'b', 'a' | 'c'>

So if this problem can "recursively trigger the joint type allocation rate", it will be solved. But the trigger condition must have two generic types, and only one is passed in the title, so we have to create a second generic type and make its default value equal to the first one:

 type Permutation<T, U = T>

So for this question, it will be expanded as follows:

 Permutation<'A' | 'B' | 'C'>
// 等价于
Permutation<'A' | 'B' | 'C', 'A' | 'B' | 'C'>
// 等价于
Permutation<'A', 'A' | 'B' | 'C'> | Permutation<'B', 'A' | 'B' | 'C'> | Permutation<'C', 'A' | 'B' | 'C'>

For Permutation<'A', 'A' | 'B' | 'C'> , excluding the combination of itself, it can form 'A', 'B' , 'A', 'C' combination, then just recurse again, spell it again, and combine the existing Excluded, the full arrangement of A is formed, and so on, to form the full arrangement of all letters.

Two things to note here:

  1. How to exclude yourself? Exclude<T, P>正合适, T联合类型P中时,会返回never ,否则返回T .
  2. When does recursion end? Use Exclude<U, T> for each recursion to leave unused combinations. After the last combination is used up, there will be left never , and the recursion is terminated at this time.
 // 本题答案
type Permutation<T, U = T> = [T] extends [never] ? [] : T extends U ? [T, ...Permutation<Exclude<U, T>>] : []

To verify the answer, first expand Permutation<'A', 'B', 'C'> :

 'A' extends 'A' | 'B' | 'C' ? ['A', ...Permutation<'B' | 'C'>] : []
'B' extends 'A' | 'B' | 'C' ? ['B', ...Permutation<'A' | 'C'>] : []
'C' extends 'A' | 'B' | 'C' ? ['C', ...Permutation<'A' | 'B'>] : []

Let's expand the first line Permutation<'B' | 'C'> :

 'B' extends 'B' | 'C' ? ['B', ...Permutation<'C'>] : []
'C' extends 'B' | 'C' ? ['C', ...Permutation<'B'>] : []

Expand the first line Permutation<'C'> :

 'C' extends 'C' ? ['C', ...Permutation<never>] : []

The full permutation is done at this point, but we still have to process Permutation<never> to make it return [] and terminate the recursion. So why use [T] extends [never] instead of T extends never ?

If we use T extends never instead of the answer to this question, the output will be never for the following reasons:

 type X = never extends never ? 1 : 0 // 1

type Custom<T> = T extends never ? 1 : 0
type Y = Custom<never> // never

Theoretically the same code, why does the output become never after using generics? The reason is that when TS is doing T extends never ? , it will allocate the union type. At this time, there is a special case, that is, when T = never , it will skip the allocation and return directly to T itself, so the ternary judgment code is not actually executed.

[T] extends [never] This way of writing can avoid TS to allocate the union type, and then bypass the above problem.

Length of String

Implementation LengthOfString<T> returns the length of the string T:

 LengthOfString<'abc'> // 3

To solve this problem, you need to know a premise, that is, the [length] attribute of the TS access array type can get the length value:

 ['a','b','c']['length'] // 3

That is, we need to convert 'abc' to ['a', 'b', 'c'] .

The second prerequisite to understand is that when using infer to refer to a string, the first refers to the first letter, and the second refers to all the rest of the letters:

 'abc' extends `${infer S}${infer E}` ? S : never // 'a'

Where does the converted array exist? Similar to js, we can get the second default value generic storage:

 // 本题答案
type LengthOfString<S, N extends any[] = []> = S extends `${infer S}${infer E}` ? LengthOfString<E, [...N, S]> : N['length']

The idea is to take out the first letter of the string and put it into the first item of the array N until the string is finished, and directly take the length of the array at this time.

Flatten

Implementation type Flatten :

 type flatten = Flatten<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, 5]

This problem requires recursion at first glance:

 // 本题答案
type Flatten<T extends any[], Result extends any[] = []> = T extends [infer Start, ...infer Rest] ? (
  Start extends any[] ? Flatten<Rest, [...Result, ...Flatten<Start>]> : Flatten<Rest, [...Result, Start]>
) : Result

This question seems to have a complicated answer, but in fact it still uses the routine of the previous question: if you need to store temporary variables during recursion, use generic default values to store them .

In this question, we use Result this generic type stores the result after leveling, every time we get the first value of the array, if the first value is not an array, it will be stored directly to continue the recursion, at this T naturally is the remaining Rest ; if the first value is an array, it will be leveled. At this time, there is a wonderful place, namely ...Start after leveling It may still be an array, such as [[5]] with two layers, can you think of ...Flatten<Start> Continuing to reuse recursion is the key to solving the problem.

Append to object

Implementation AppendToObject :

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

Combined with the previous experience of brushing the question, the solution to this question is very simple. Note that K in Key can expand some specified keys for the object:

 // 本题答案
type AppendToObject<Obj, Key extends string, Value> = Obj & {
  [K in Key]: Value
}

Of course, there is also a way of writing that does not use Obj & , that is, the description method that combines the original object with the new Key and Value:

 // 本题答案
type AppendToObject<T, U extends number | string | symbol, V> = {
  [key in (keyof T) | U]: key extends U ? V : T[Exclude<key, U>]
}

Absolute

Implementation Absolute convert numbers to absolute values:

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

The point of this question is to convert numbers into absolute value strings, so we can use strings to match:

 // 本题答案
type Absolute<T extends number> = `${T}` extends `-${infer R}` ? R : `${T}`

Why not use T extends to judge? Because T is a number, writing a string description that cannot match the symbol.

String to Union

Implementation StringToUnion convert string to union type:

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

Or the old way, use a new generic to store the answer, recursive:

 // 本题答案
type StringToUnion<T, P = never> = T extends `${infer F}${infer R}` ? StringToUnion<R, P | F> : P

Of course, you can also store the answer without relying on generics, because this question is special, you can use | directly:

 // 本题答案
type StringToUnion<T> = T extends `${infer F}${infer R}` ? F | StringToUnion<R> : never

Merge

Implementation Merge merge two objects, the latter takes precedence in case of conflict:

 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}

The answer to this question is even the solution steps of the previous question, that is, using an object to describe the thinking of + keyof :

 // 本题答案
type Merge<A extends object, B extends object> = {
  [K in keyof A | keyof B] : K extends keyof B ? B[K] : (
    K extends keyof A ? A[K] : never
  )
}

As long as you know that in keyof supports tuples, and the value part is distinguished by extends , it is very simple.

KebabCase

The function that realizes hump to horizontal line KebabCase :

 KebabCase<'FooBarBaz'> // 'foo-bar-baz'

Or the old routine, use the second parameter to store the result, traverse the string recursively, convert the uppercase letter to lowercase and add - , and finally put the beginning - Just do it:

 // 本题答案
type KebabCase<S, U extends string = ''> = S extends `${infer F}${infer R}` ? (
  Lowercase<F> extends F ? KebabCase<R, `${U}${F}`> : KebabCase<R, `${U}-${Lowercase<F>}`>
) : RemoveFirstHyphen<U>

type RemoveFirstHyphen<S> = S extends `-${infer Rest}` ? Rest : S

It is very easy to understand if it is written separately. First of all KebabCase The first character is recursively taken each time. How to judge whether this character is uppercase? As long as the lowercase is not equal to the original value, it is uppercase, so the judgment condition is the false branch of Lowercase<F> extends F . Then write a function RemoveFirstHyphen to kill the first string - .

Summarize

TS is a programming language, not a simple description or modifier. Many complex types of problems need to be implemented using logical thinking, rather than simply checking syntax.

The discussion address is: Intensive Reading "Permutation, Flatten, Absolute..." Issue #426 dt-fe/weekly

If you'd like to join the discussion, click here , there are new topics every week, with a weekend or Monday release. Front-end intensive reading - help you filter reliable content.

Follow Front-end Intensive Reading WeChat Official Account

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

Copyright notice: Free to reprint - non-commercial - non-derivative - keep attribution ( Creative Commons 3.0 license )

黄子毅
7k 声望9.5k 粉丝