1

The best way to solve the TS problem is to practice more. This time, the difficulty of interpreting type-challenges Medium is 25~32 questions.

intensive reading

Diff

Implements Diff<A, B> and returns a new object of type Diff of two object types:

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

Equal<Diff<Foo, Bar> // { gender: number }

First of all, we must think about the calculation method of Diff. The Diff between A and B is to find that A exists but B does not exist, and B has a value that does not exist in A, then just use the Exclude<X, Y> function, which can get the value that exists in XY的值,我们只要用keyof Akeyof B XY , and alternate A and B positions to get Diff:

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

We also mentioned the tricks in the Value part before, that is, two sets of ternary operators are needed to ensure that the subscripts accessed exist in the object, that is, the syntax tricks of extends keyof .

AnyOf

AnyOf函数,任意项为真则返回true ,否则返回false ,空数组返回false

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

There are several questions to think about in this question:

First, what kind of judgment is used? Like this kind of problem of judging whether any element in the array satisfies a certain condition, it can be solved in a recursive way. Specifically, the first item of the array is judged first, and if it is satisfied, the remaining items are continued to be judged recursively, otherwise the judgment is terminated. This can be done, but it is more troublesome. Another trick is to use the method of extends Array<> to let TS automatically traverse for you.

The second is how to judge any item is true?为真的情况很多,我们尝试枚举为假的Case: 0 undefined '' undefined null never [] .

Combining the above two thoughts, it is not difficult to think of the answer to this question as follows:

 type Falsy = '' | never | undefined | null | 0 | false | []
type AnyOf<T extends readonly any[]> = T extends Falsy[] ? false : true

But this test case fails:

 AnyOf<[0, '', false, [], {}]>

If you add {} to Falsy 67d1504bc581f9de772bf00c49dc8069--- at this time, you will find that except this case, all other judgments are hung up, the reason is { a: 1 } extends {} the result is true, {} does not mean empty objects, but all object types, so we'll replace it with Record<PropertyKey, never> to lock empty objects:

 // 本题答案
type Falsy = '' | never | undefined | null | 0 | false | [] | Record<PropertyKey, never>
type AnyOf<T extends readonly any[]> = T extends Falsy[] ? false : true

IsNever

Implement IsNever to determine whether the value type is never :

 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

First of all, we can write down a wrong answer without hesitation:

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

This wrong answer is definitely closer to the correct answer, but the fault lies in the inability to judge never . In the Permutation full permutation problem, we realized the speciality of never in generics, it will not trigger the extends judgment, but directly terminate, causing Judgment is invalid.

The solution is also very simple, just bypass the feature of never and wrap an array:

 // 本题答案
type IsNever<T> = [T] extends [never] ? true :false

IsUnion

Implementation IsUnion determine whether it is a union type:

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

This question is a complete brain teaser, because TS definitely knows whether the incoming type is a union type, and will perform special processing on the union type, but it does not expose the judgment syntax of the union type, so we can only do the incoming type. Test, infer whether it is a union type.

There are only two characteristics of union types that we can think of so far:

  1. When the TS handles the generic type as a union type, the distribution process is performed, that is, the union type is disassembled into independent items to be determined one by one, and finally | is used to connect.
  2. Wrap the union type with [] to circumvent the distribution feature.

So how do you determine that the incoming generic is a union type? If the generic is distributed, it can be inferred that it is a union type.

The difficulty shifts to: how to judge the generic type is distributed? First of all, let's analyze what the distribution effect looks like:

 A extends A
// 如果 A 是 1 | 2,分发结果是:
(1 extends 1 | 2) | (2 extends 1 | 2)

That is, this expression will be executed twice, the first A in the two values are 1 and 2 , and the second A in both executions each time is 1 | 2 , but both expressions are true , which cannot reflect the specificity of distribution.

At this time, the package [] is not distributed, that is, after distribution, because in each execution process, the first one A is a certain item of the joint type, so用[]与原始值不相等,所以我们在extends 65f84d71036b82c9dcd504a3c8e1f51f---分发过程中, [] a54c0db41fab4c2d625a82704a74ddf0---包裹---e9163e4a740b3bcde80d7e03a92ab980 extends , If there is no match at this time, it means that a distribution has occurred:

 type IsUnion<A> = A extends A ? (
  [A] extends [A] ? false : true
) : false

But this code is still incorrect, because inside the first ternary expression bracket, A has been distributed, so [A] extends [A] is true even for union types, this When you need to replace the original value extends behind [A] , the Sao operation appears:

 type IsUnion<A, B = A> = A extends A ? (
  [B] extends [A] ? false : true
) : false

B = A ,但过程中因为A被分发了, BA的,才enable us to achieve our goals. [B] put extends , B is not distributed and cannot be included in the distributed result, so this condition must be false during distribution.

Finally, because the test case has a never situation, we can use the IsNever function to judge in advance:

 // 本题答案
type IsUnion<A, B = A> = IsNever<A> extends true ? false : (
  A extends A ? (
    [B] extends [A] ? false : true
  ) : false
)

From this question, we can deeply appreciate the weirdness of TS, namely type X<T> = T extends ... middle extends front T not necessarily the incoming T , if it is a union type, it will be distributed as a single type and processed separately.

ReplaceKeys

ReplaceKeys<Obj, Keys, Targets> 016381e880a2ed33355eb4f4f81bfbcb---将Obj Keys Key 类型转化为符合Targets Key 描述的类型,如果无法匹配To Targets then the type is set to never :

 type NodeA = {
  type: 'A'
  name: string
  flag: number
}

type NodeB = {
  type: 'B'
  id: number
  flag: number
}

type NodeC = {
  type: 'C'
  name: string
  flag: number
}


type Nodes = NodeA | NodeB | NodeC

type ReplacedNodes = ReplaceKeys<Nodes, 'name' | 'flag', {name: number, flag: string}> // {type: 'A', name: number, flag: string} | {type: 'B', id: number, flag: string} | {type: 'C', name: number, flag: string} // would replace name from string to number, replace flag from number to string.

type ReplacedNotExistKeys = ReplaceKeys<Nodes, 'name', {aa: number}> // {type: 'A', name: never, flag: number} | NodeB | {type: 'C', name: never, flag: number} // would replace name to never

Although the description of this question is very scary, it is actually very simple. The idea is to use K in keyof Obj to traverse all the keys of the original object. If this key is in the description Keys , and in Targets exists, then the return type Targets[K] otherwise return never , if it is not described in Keys , the original type is used in the object:

 // 本题答案
type ReplaceKeys<Obj, Keys, Targets> = {
  [K in keyof Obj] : K extends Keys ? (
    K extends keyof Targets ? Targets[K] : never
  ) : Obj[K]
}

Remove Index Signature

Implement RemoveIndexSignature<T> Remove the Index subscript in the object <T> :

 type Foo = {
  [key: string]: any;
  foo(): void;
}

type A = RemoveIndexSignature<Foo>  // expected { foo(): void }

The focus of this question is how to identify the object string Key. You can use \`${infer P}\` to identify whether P to determine whether the string Key is currently hit:

 // 本题答案
type RemoveIndexSignature<T> = {
  [K in keyof T as K extends `${infer P}` ? P : never]: T[K]
}

Percentage Parser

Implement PercentageParser<T> , and parse out the sign bit and number of the percent string:

 type PString1 = ''
type PString2 = '+85%'
type PString3 = '-85%'
type PString4 = '85%'
type PString5 = '85'

type R1 = PercentageParser<PString1> // expected ['', '', '']
type R2 = PercentageParser<PString2> // expected ["+", "85", "%"]
type R3 = PercentageParser<PString3> // expected ["-", "85", "%"]
type R4 = PercentageParser<PString4> // expected ["", "85", "%"]
type R5 = PercentageParser<PString5> // expected ["", "85", ""]

This question fully shows that TS has no regular ability, try not to do regular things ^_^.

Back to the topic, if we have to use TS to implement, we can only enumerate various scenarios:

 // 本题答案
type PercentageParser<A extends string> = 
  // +/-xxx%
  A extends `${infer X extends '+' | '-'}${infer Y}%`? [X, Y, '%'] : (
    // +/-xxx
    A extends `${infer X extends '+' | '-'}${infer Y}` ? [X, Y, ''] : (
      // xxx%
      A extends `${infer X}%` ? ['', X, '%'] : (
        // xxx 包括 ['100', '%', ''] 这三种情况
        A extends `${infer X}` ? ['', X, '']: never
      )
    )
  )

This question uses the knowledge that infer can make infinite branch judgments.

Drop Char

The implementation DropChar removes the specified character from the string:

 type Butterfly = DropChar<' b u t t e r f l y ! ', ' '> // 'butterfly!'

This question is very similar to Replace , as long as recursion is used to continuously exclude C :

 // 本题答案
type DropChar<S, C extends string> = S extends `${infer A}${C}${infer B}` ? 
  `${A}${DropChar<B, C>}` : S

Summarize

Writing this, I feel more and more that although TS has Turing completeness, it is not as convenient as JS in terms of logic processing. The solutions to many problems in designing computational logic are not very elegant.

However, solving such problems will help strengthen the understanding and comprehensive application of the TS basic ability combination, and it is also essential when solving practical problems.

The discussion address is: Intensive Reading "Diff, AnyOf, IsUnion..." Issue #429 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 粉丝