7

TS strong typing is very easy to use, but in practical application, it is inevitable to encounter some problems that are difficult to describe and cannot be solved by reading the official documents repeatedly. So far, there is no document or a set of textbooks that can solve all types of corners and corners. question. Why is this so? Because TS is not a simple annotator, but a Turing-complete language, the solutions to many problems are hidden in the basic ability, but you may not be able to think of it when you have learned the basic ability.

The best way to solve this problem is to practice more, and constantly stimulate your brain through actual cases, so that you can develop TS thinking habits. So without further ado, let's start today with the Easy difficulty of type-challenges .

intensive reading

Pick

Manually implement the built-in Pick<T, K> function, which returns a new type and extracts type K from object T:

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

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}

It is easier to understand with the example, that is, K is a string, we need to return a new type, and only keep the Key defined by K .

The first difficulty is how to limit the value of K , for example, if a value that does not exist in T is passed in, an error will be reported. This investigation is hard knowledge, as long as you know A extends keyof B this grammar can be associated with it.

The second difficulty lies in how to generate a type that only contains K to define the Key, you must first know that there is { [A in keyof B]: B[A] } this hard knowledge, so that you can reassemble an object:

 // 代码 1
type Foo<T> = {
  [P in keyof T]: T[P]
}

Only knowing this syntax may not be able to come up with ideas, the reason is that you have to break the stereotyped understanding of TS, [K in keyof T] is not a fixed template, of which keyof T is just a reference variable, it can is replaced, if you replace it with a variable of another range, then the key value range of this object will change, which is exactly in line with this question K :

 // 代码 2(本题答案)
type MyPick<T, K in keyof T> = {
  [P in K]: T[P]
}

Don't look at this question as simple as knowing the answer, but it is still rewarding to review it. Comparing the above two code examples, you will find that the code 1 keyof T is only mentioned in the generic definition from the object description, so there is no change in function, but because generics can be defined by The user passed in, so code 1's P in keyof T because there is no generic support, what is derived here is all the keys of T , and code 2 although the code is moved to the generic, but because The extends description is used, so it means that the type of P is constrained to the Keys of T . As for what it is, it depends on how the user code is passed.

So in fact, there is no default value in the generic type K , and the default value is written into the object as a deduced value. The way to give default values in generics is as follows:

 // 代码 3
type MyPick<T, K extends keyof T = keyof T> = {
  [P in K]: T[P]
}

That is to say, in this way MyPick<Todo> can also work correctly and return Todo type, that is, code 3 is the same as code 1 when the second parameter is not passed. The function is exactly the same. Think carefully about the commonalities and differences, why Code 3 can achieve the same function as Code 1, and has stronger expansibility, your understanding of the actual combat of TS generics has reached a higher level.

Readonly

Manually implement the built-in Readonly<T> function to set all properties of the object to read-only:

 interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property

This question is simpler than the first question, as long as we redeclare the object with { [A in keyof B]: B[A] } , and add readonly in front of each Key modification:

 // 本题答案
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
}

According to this feature, we can do a lot of extended transformation, such as setting all keys of the object as optional:

 type Optional<T> = {
  [K in keyof T]?: T[K]
}

{ [A in keyof B]: B[A] } us the opportunity to describe the details of each Key attribute, and limits our imagination.

First Of Array

Implementation type First<T> , get the type of the first item of the array:

 type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3

This question is relatively simple, and the answer is easy to think of:

 // 本题答案
type First<T extends any[]> = T[0]

But when I wrote this answer, 10% of my brain cells reminded me that I didn't judge the boundary situation, and I really read the answer. I have to consider the case of an empty array. When an empty array is used, the return type never not undefined will be better, the following are the answers:

 type First<T extends any[]> = T extends [] ? never : T[0]
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]
type First<T> = T extends [infer P, ...infer Rest] ? P : never

The first way of writing is to judge whether T is an empty array by extends [] , and if so, return never .

The second way of writing is to judge the empty array by the length of 0. At this time, you need to understand two points: 1. You can let TS access the value length (type) through T['length'] , 2. extends 0 Indicates whether to match 0, that is, extends in addition to matching types, it can also directly match values.

The third way of writing is the most worry-free, but it also uses the infer keyword. Even if you fully know how to use infer ( intensively read "Typescript infer Keywords" ), it is difficult think of it. The reason for using infer is that there are boundary conditions in this scene, and the most understandable way to write it is "if the T shape is like <P, ...> ", then I will return the type P , Otherwise, return never ", which is described in TS as: T extends [infer P, ...infer Rest] ? P : never .

Length of Tuple

Implementation type Length<T> Get tuple length:

 type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']

type teslaLength = Length<tesla>  // expected 4
type spaceXLength = Length<spaceX> // expected 5

After studying the previous question, it is easy to think of this answer:

 type Length<T extends any[]> = T['length']

For TS, tuples and arrays are both arrays, but tuples can observe their lengths for TS, T['length'] For tuples, the specific values are returned, while for arrays is number .

Exclude

Implementation type Exclude<T, U> , returns the part of T that does not exist in U . This function is mainly used in joint type scenarios, so we can directly use extends to judge:

 // 本题答案
type Exclude<T, U> = T extends U ? never : T

Actual running effect:

 type C = Exclude<'a' | 'b', 'a' | 'c'> // 'b'

It seems a little bit incomprehensible, because TS's implementation of union types is allocative, ie:

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

Awaited

Implementation type Awaited , for example, get ---36b2ff8de451814f6a1800088e40e98a--- from Promise<ExampleType> ExampleType .

First of all, TS will never execute code, so don't have "await wait until you know the result" in your head. The key to this question is to extract the type T Promise<T> which is very suitable for infer to do:

 type MyAwaited<T> = T extends Promise<infer U> ? U : never

However, this answer is not standard enough, the standard answer considers the nested Promise scenario:

 // 该题答案
type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer P>
  ? P extends Promise<unknown> ? MyAwaited<P> : P
  : never

If Promise<P> P 4f2c3c484eaf2149d4ff352ed8cb2b9a--- and it looks like Promise<unknown> , call itself MyAwaited<P> Recursion is mentioned here, that is, TS type processing can be recursive, so there is a later version for tail recursion optimization.

If

实现类型If<Condition, True, False> ,当C true时返回T ,否则返回F

 type A = If<true, 'a', 'b'>  // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'

As mentioned before, extends can also be used to determine the value, so decisively use extends true to determine whether the hit true can be:

 // 本题答案
type If<C, T, F> = C extends true ? T : F

Concat

Using the type system implementation Concat<P, Q> , concatenate the two array types:

 type Result = Concat<[1], [2]> // expected to be [1, 2]

Since TS supports array destructuring syntax, you can boldly try to write:

 type Concat<P extends any[], Q extends any[]> = [...P, ...Q]

Considering that Concat functions should also be able to receive non-array types, so to make a judgment, in order to facilitate writing, extends from the generic definition position to the runtime of TS type inference:

 // 本题答案
type Concat<P, Q> = [
  ...P extends any[] ? P : [P],
  ...Q extends any[] ? Q : [Q],
]

Solving this problem requires belief that TS can write logic like JS. These capabilities are provided incrementally during version upgrades, so it is still difficult to constantly read the latest TS features and quickly understand them as solidified knowledge.

Includes

Implement the Includes<T, K> function with the type system:

 type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`

Due to previous experience, it is easy to make the following associations:

 // 如果题目要求是这样
type isPillarMen = Includes<'Kars' | 'Esidisi' | 'Wamuu' | 'Santana', 'Dio'>
// 那我就能用 extends 轻松解决了
type Includes<T, K> = K extends T ? true : false

It is a pity that the first input is an array type, extends does not support the judgment "array contains" logic. At this time, we need to understand a new knowledge point, that is, the [number] subscript in the TS judgment. Not only this question, but many difficult questions in the future will require it as basic knowledge.

[number] The subscript indicates any item, and extends T[number] can realize the determination of array inclusion, so the following solution is valid:

 type Includes<T extends any[], K> = K extends T[number] ? true : false

But after flipping through the answer, I found that this is not the standard answer, and I really found a counter example:

 type Includes<T extends any[], K> = K extends T[number] ? true : false
type isPillarMen = Includes<[boolean], false> // true

原因很简单, truefalse都继承自boolean --- ,所以---3adb9f1c2dd2b1e61c3c2431fecb0c14 extends判断的界限太宽了,题目要求的是精确The values match, so the answer above is theoretically wrong.

The standard answer is to judge the first item of the array each time, and recurse (to tell the truth, this is not an easy question), there are two difficulties respectively.

First how to write the Equal function? The more popular solution is this:

 type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

There was also a small discussion about how to write the Equal function. The above code constructs two functions. The T in these two functions belongs to the type of deferred judgment isTypeIdenticalTo which depends on the internal- isTypeIdenticalTo function completes the judgment.

With Equal it's easy, we can do it with destructuring + infer + recursion:

 // 本题答案
type Includes<T extends any[], K> =
  T extends [infer F, ...infer Rest] ?
    Equal<F, K> extends true ?
      true
      : Includes<Rest, K>
    : false

Each time the first value of the array is taken to judge Equal , if it does not match, the remaining items will be judged recursively. This function combines a lot of TS knowledge, such as:

  • recursion
  • deconstruct
  • infer
  • extends true

It can be found that in order to solve the problem of true extends boolean for true , we went around a lot and used a more complicated method to achieve it, which is also normal in TS gymnastics, and solve the problem Patience is required.

Push

Implementation Push<T, K> function:

 type Result = Push<[1, 2], '3'> // [1, 2, '3']

This problem is really simple, just use deconstruction:

 // 本题答案
type Push<T extends any[], K> = [...T, K]

It can be seen that if you want to easily solve a simple TS problem, you first need to be able to solve some difficult problems 😁.

Unshift

Implement Unshift<T, K> function:

 type Result = Unshift<[1, 2], 0> // [0, 1, 2,]

Just change the order on the basis of Push :

 // 本题答案
type Unshift<T extends any[], K> = [K, ...T]

Parameters

Implement the built-in function Parameters :

Parameters You can get the parameter type of the function and directly use infer to realize it, and it is relatively simple:

 type Parameters<T> = T extends (...args: infer P) => any ? P : []

infer can be easily taken from any specific position, which is a typical difficult-to-understand and easy-to-use syntax.

Summarize

After learning the basic grammar of TS, the key is to use it.

The discussion address is: Intensive reading "type challenges - easy" Issue #422 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 粉丝