1

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

intensive reading

MinusOne

Implemented in TS MinusOne decreases a number by one:

 type Zero = MinusOne<1> // 0
type FiftyFour = MinusOne<55> // 54

TS does not have "ordinary" computing power, but there is a way to deal with numbers, that is, TS can access the length of the array through ['length'] , and almost all numerical calculations are derived from it.

For this question, we only need to construct an array whose length is generic length-1, and get its ['length'] attribute, but this scheme has a flaw and cannot calculate negative values, because the length of the array cannot be less than 0:

 // 本题答案
type MinusOne<T extends number, arr extends any[] = []> = [
  ...arr,
  ''
]['length'] extends T
  ? arr['length']
  : MinusOne<T, [...arr, '']>

The principle of this scheme is not the original number -1, but continuously adding 1 from 0 to the target number minus one. But this scheme failed the MinusOne<1101> test, because 1000 recursion times is the upper limit.

There is another way to break the recursion, namely:

 type Count = ['1', '1', '1'] extends [...infer T, '1'] ? T['length'] : 0 // 2

That is, convert minus one to extends [...infer T, '1'] , so that the length of the array T is exactly equal to the answer. Then the difficulty becomes how to construct an array of equal length based on the incoming numbers? That is, the problem becomes how to implement CountTo<N> to generate an array with a length of N , each item is 1 , and the recursive efficiency of generating an array is also high, Otherwise, you will encounter the recursion upper limit problem.

There is a fairy solution on the Internet, the author can't think of it, but I can take it out for everyone to analyze:

 type CountTo<
  T extends string,
  Count extends 1[] = []
> = T extends `${infer First}${infer Rest}`
  ? CountTo<Rest, N<Count>[keyof N & First]>
  : Count

type N<T extends 1[] = []> = {
  '0': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]
  '1': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1]
  '2': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1]
  '3': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1, 1]
  '4': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, 1, 1, 1, 1]
  '5': [
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    1,
    1,
    1,
    1,
    1
  ]
  '6': [
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    1,
    1,
    1,
    1,
    1,
    1
  ]
  '7': [
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    1,
    1,
    1,
    1,
    1,
    1,
    1
  ]
  '8': [
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    1,
    1,
    1,
    1,
    1,
    1,
    1,
    1
  ]
  '9': [
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    ...T,
    1,
    1,
    1,
    1,
    1,
    1,
    1,
    1,
    1
  ]
}

That is, this method can be implemented efficiently CountTo<'1000'> to generate an array of length 1000, each item is 1 , more specifically, only need to traverse <T> string length The number of times, such as 1000 only needs to recurse 4 times, and 10000 also needs to recurse only 5 times.

CountTo函数体的逻辑是, T非空,就拆First Rest , then take the remaining characters recursively, but generate First to the correct length in one go. The core logic is the function N<T> . What it actually does is to enlarge the length of the array T by 10 times, and then append the current number of 1 to the end of the array.

And keyof N & First is also a stroke of magic, the original intention here is to access the First subscript, but TS does not know it is a safe and accessible subscript, and the final value of keyof N & First Or First , which can also be recognized as a subscript by TS security.

Take CountTo<'123'> for example:

The first execution First='1' , Rest='23' :

 CountTo<'23', N<[]>['1']>
// 展开时,...[] 还是 [],所以最终结果为 ['1']

The second execution First='2' , Rest='3'

 CountTo<'3', N<['1']>['2']>
// 展开时,...[] 有 10 个,所以 ['1'] 变成了 10 个 1,追加上 N 映射表里的 2 个 1,现在一共有 12 个 1

The third execution First='3' , Rest=''

 CountTo<'', N<['1', ...共 12 个]>['3']>
// 展开时,...[] 有 10 个,所以 12 个 1 变成 120 个,加上映射表中 3,一共有 123 个 1

To sum up, it is to turn the number T into a string, get it from the far left, multiply the accumulated number of arrays by 10 each time, and then add 1 of the current number of values to achieve extremely recursive times. greatly reduced.

PickByType

Implement PickByType<P, Q> and keep the key of type Q ae4419cda62b77b7f9a5add2b130a53e--- in the object P :

 type OnlyBoolean = PickByType<
  {
    name: string
    count: number
    isReadonly: boolean
    isEnable: boolean
  },
  boolean
> // { isReadonly: boolean; isEnable: boolean; }

This question is very simple, because when we encountered the Remove Index Signature question before, we used K in keyof P as xxx to further judge the key position, so as long as P[K] extends Q is reserved, otherwise it will return never can:

 // 本题答案
type PickByType<P, Q> = {
  [K in keyof P as P[K] extends Q ? K : never]: P[K]
}

StartsWith

Implement StartsWith<T, U> to determine whether the string T U :

 type a = StartsWith<'abc', 'ac'> // expected to be false
type b = StartsWith<'abc', 'ab'> // expected to be true
type c = StartsWith<'abc', 'abcd'> // expected to be false

This problem is also relatively simple, it can be solved with recursion + first character equality:

 // 本题答案
type StartsWith<
  T extends string,
  U extends string
> = U extends `${infer US}${infer UE}`
  ? T extends `${infer TS}${infer TE}`
    ? TS extends US
      ? StartsWith<TE, UE>
      : false
    : false
  : true

The idea is:

  1. U一切场景,直接返回trueU US (U Start) The string at the beginning, UE (U End) is used for subsequent judgment.
  2. Following the above judgment, if T is an empty string, it cannot be matched by U T directly return false ; Follow-up judgment is made for the character string starting with TS (T Start) and TE (T End).
  3. Following the above judgment, if TS extends US indicates that the first character matches this time, then recursively matches the remaining characters StartsWith<TE, UE> , if the first character does not match, return in advance false .

After reading some answers, the author found that there is also a dimensionality reduction attack scheme:

 // 本题答案
type StartsWith<T extends string, U extends string> = T extends `${U}${string}`
  ? true
  : false

Unexpectedly, you can also use ${string} to match any string for extends judgment, which is a bit regular. Of course, ${string} can also be replaced by ${infer X} , but the obtained X does not need to be used anymore:

 // 本题答案
type StartsWith<T extends string, U extends string> = T extends `${U}${infer X}`
  ? true
  : false

The author also tried the following answer, which is also correct when the suffix Diff part is string like number:

 // 本题答案
type StartsWith<T extends string, U extends string> = T extends `${U}${number}`
  ? true
  : false

The most common reference for string templates is ${infer X} or ${string} , if you want to match a specific numeric string, you can also mix ${number} .

EndsWith

Implement EndsWith<T, U> to determine whether the string T U :

 type a = EndsWith<'abc', 'bc'> // expected to be true
type b = EndsWith<'abc', 'abc'> // expected to be true
type c = EndsWith<'abc', 'd'> // expected to be false

With the experience of the above question, this question should not be too simple:

 // 本题答案
type EndsWith<T extends string, U extends string> = T extends `${string}${U}`
  ? true
  : false

It can be seen that the skills of TS are very simple to master, but there is almost no solution if you don't know it, or you can solve it with stupid recursion.

PartialByKeys

PartialByKeys<T, K> ,使K的Key 变成可选的定义,如果不传K效果与Partial<T>

 interface User {
  name: string
  age: number
  address: string
}

type UserPartialName = PartialByKeys<User, 'name'> // { name?:string; age:number; address:string }

Seeing that the question requires no parameters to be passed and the behavior of Partial<T> has been consistent, you should be able to think of writing a default value like this:

 type PartialByKeys<T, K = keyof T> = {}

We have to use optional and non-optional to describe the two objects together, because TS does not support two keyof descriptions under the same object, so it can only be written as two objects:

 type PartialByKeys<T, K = keyof T> = {
  [Q in keyof T as Q extends K ? Q : never]?: T[Q]
} & {
  [Q in keyof T as Q extends K ? never : Q]: T[Q]
}

But it does not match the test case, because the final type is correct, but because it is divided into two objects and merged into one object, it needs to be merged with a little Magic behavior:

 // 本题答案
type PartialByKeys<T, K = keyof T> = {
  [Q in keyof T as Q extends K ? Q : never]?: T[Q]
} & {
  [Q in keyof T as Q extends K ? never : Q]: T[Q]
} extends infer R
  ? {
      [Q in keyof R]: R[Q]
    }
  : never

It seems pointless to expand an object extends infer R again, but it does make the type merge into one object, which is very interesting. We can also extract it into a function Merge<T> to use.

This question also has an answer to function composition:

 // 本题答案
type Merge<T> = {
  [K in keyof T]: T[K]
}
type PartialByKeys<T, K extends PropertyKey = keyof T> = Merge<
  Partial<T> & Omit<T, K>
>
  • Use Partial & Omit to merge objects.
  • Omit<T, K>K有来自于keyof T的限制,而测试用例又包含---81285e97e6ba269a0492d9e2911dbfae---这种不存在的Key 值, unknown You can use extends PropertyKey to handle this scenario.

RequiredByKeys

Implement RequiredByKeys<T, K> , so that the matching Key of K becomes a mandatory definition, if not pass K the effect is the same as Required<T>

 interface User {
  name?: string
  age?: number
  address?: string
}

type UserRequiredName = RequiredByKeys<User, 'name'> // { name: string; age?: number; address?: string }

Contrary to the above question, the answer is also ready to come out:

 type Merge<T> = {
  [K in keyof T]: T[K]
}
type RequiredByKeys<T, K extends PropertyKey = keyof T> = Merge<
  Required<T> & Omit<T, K>
>

Wait, not a single test case passed, why? Think about it carefully and find that there is indeed a hidden mystery:

 Merge<{
  a: number
} & {
  a?: number
}> // 结果是 { a: number }

That is, when the same Key is optional and required at the same time, the combined result is required. In the previous question, because the mandatory Omit is dropped, the optional will not be covered by the mandatory, but this question Merge<Required<T> & Omit<T, K>> , the previous Required<T> required priority The highest, the latter Omit<T, K> although the logic itself is correct, it cannot override the mandatory selection as optional, so the test cases are all hung up.

The solution is to crack this feature, use the original object & only the mandatory object containing K , so that the mandatory option covers the previous optional Key. The latter can Pick come out:

 type Merge<T> = {
  [K in keyof T]: T[K]
}
type RequiredByKeys<T, K extends PropertyKey = keyof T> = Merge<
  T & Required<Pick<T, K>>
>

This leaves one single test that fails:

 Expect<Equal<RequiredByKeys<User, 'name' | 'unknown'>, UserRequiredName>>

We also need to be compatible with Pick to access a non-existent Key, use extends to avoid it:

 // 本题答案
type Merge<T> = {
  [K in keyof T]: T[K]
}
type RequiredByKeys<T, K extends PropertyKey = keyof T> = Merge<
  T & Required<Pick<T, K extends keyof T ? K : never>>
>

Mutable

Implement Mutable<T> and make all keys of the object T writable:

 interface Todo {
  readonly title: string
  readonly description: string
  readonly completed: boolean
}

type MutableTodo = Mutable<Todo> // { title: string; description: string; completed: boolean; }

Change an object from non-writable to writable:

 type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}

It is also easy to change from writable to non-writable, mainly depends on whether you remember this syntax: -readonly :

 // 本题答案
type Mutable<T extends object> = {
  -readonly [K in keyof T]: T[K]
}

OmitByType

Implementation OmitByType<T, U> Exclude Keys in T according to type U:

 type OmitBoolean = OmitByType<
  {
    name: string
    count: number
    isReadonly: boolean
    isEnable: boolean
  },
  boolean
> // { name: string; count: number }

This question and PickByType just reversed, just swap the content after extends :

 // 本题答案
type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K]
}

Summarize

This week's topics are relatively common, except for MinusOne that the fairy solution is more difficult. Among them, Merge The magic of the function needs to be understood.

The discussion address is: Intensive Reading "MinusOne, PickByType, StartsWith..." Issue #430 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.6k 粉丝