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

intensive reading

Get Return Type

Implement the very classic ReturnType<T> :

 const fn = (v: boolean) => {
  if (v)
    return 1
  else
    return 2
}

type a = MyReturnType<typeof fn> // should be "1 | 2"

First of all, don't be frightened by the example. You feel that you must execute the code to know the return type. In fact, TS has already deduced the return type for us, so the type of the above function fn is already like this:

 const fn = (v: boolean): 1 | 2 => { ... }

All we have to do is to extract the function return value from the inside, which is very suitable to implement with infer :

 // 本题答案
type MyReturnType<T> = T extends (...args: any[]) => infer P ? P : never

infer with extends is an artifact of deconstructing complex types. If you can't understand the above code at a glance, it means that you are right infer is not familiar enough, you need to read more.

Omit

Implement Omit<T, K> , the effect is exactly the opposite of Pick<T, K> , and exclude the object T K

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

type TodoPreview = MyOmit<Todo, 'description' | 'title'>

const todo: TodoPreview = {
  completed: false,
}

An easier way to try this problem is:

 type MyOmit<T, K extends keyof T> = {
  [P in keyof T]: P extends K ? never : T[P]
}

In fact, it still includes description , title these two keys, but the type of these two keys is never , which does not meet the requirements.

So as long as P in keyof T is written, the Key cannot be erased by any subsequent writing, we should start with the Key:

 type MyOmit<T, K extends keyof T> = {
  [P in (keyof T extends K ? never : keyof T)]: T[P]
}

But this is still wrong, our thinking is correct, that is, the exclusion of keyof T belonging to K , but because before and after keyof T is not related, so we need to Exclude Tell TS that before and after keyof T is the same reference (implemented in the previous lecture Exclude ):

 // 本题答案
type MyOmit<T, K extends keyof T> = {
  [P in Exclude<keyof T, K>]: T[P]
}

type Exclude<T, U> = T extends U ? never : T

This is correct, the core of mastering the problem is:

  1. The ternary judgment can also be written in the Key position.
  2. The effect is the same whether JS extracts a function or not, but TS needs to infer. In many cases, a function is extracted to tell TS that it is "the same reference".

Of course, since we have all used Exclude , we might as well combine Pick to write a more elegant Omit implementation:

 // 本题优雅答案
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

Readonly 2

Implement MyReadonly2<T, K> and make the specified Key K ReadOnly:

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

const todo: MyReadonly2<Todo, 'title' | 'description'> = {
  title: "Hey",
  description: "foobar",
  completed: false,
}

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

This question is quite difficult at first glance, because readonly must be defined in the Key position, but we cannot make a ternary judgment at this position. In fact, we used the Pick , Omit and the built-in Readonly which we made before, and it came out:

 // 本题答案
type MyReadonly2<T, K extends keyof T> = Readonly<Pick<T, K>> & Omit<T, K>

That is, we can divide the object into two parts, first Pick out K Key part is set to Readonly, and then use & to merge the remaining Key, just use To the function of the previous question Omit , perfect.

Deep Readonly

Implementation DeepReadonly<T> recursively all child elements:

 type X = { 
  x: { 
    a: 1
    b: 'hi'
  }
  y: 'hey'
}

type Expected = { 
  readonly x: { 
    readonly a: 1
    readonly b: 'hi'
  }
  readonly y: 'hey' 
}

type Todo = DeepReadonly<X> // should be same as `Expected`

This must be implemented with type recursion. Since we want to recurse, we must not rely on the built-in Readonly function, we need to expand the function by hand:

 // 本题答案
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends Object> ? DeepReadonly<T[K]> : T[K]
}

Here Object can also be replaced by Record<string, any> .

Tuple to Union

Implementation TupleToUnion<T> returns the set of all values of a tuple:

 type Arr = ['1', '2', '3']

type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'

This title converts the tuple type to the possible set of all its values, that is, we want to access this array with all the subscripts, in TS, use [number] as the subscript:

 // 本题答案
type TupleToUnion<T extends any[]> = T[number]

Chainable Options

It's better to look at the example directly:

 declare const config: Chainable

const result = config
  .option('foo', 123)
  .option('name', 'type-challenges')
  .option('bar', { value: 'Hello World' })
  .get()

// expect the type of result to be:
interface Result {
  foo: number
  name: string
  bar: {
    value: string
  }
}

That is to say, we implement a relatively complex Chainable type. Objects of this type can be called .option(key, value) in a chain until we use get() to get the aggregation. All option objects.

If we implement this function in JS, we must store the value of Object in the current closure, and then provide get to return directly, or option to recurse and pass in the new value. We might as well use Class to achieve:

 class Chain {
  constructor(previous = {}) {
    this.obj = { ...previous }
  }
  
  obj: Object
  get () {
    return this.obj
  }
  option(key: string, value: any) {
    return new Chain({
      ...this.obj,
      [key]: value
    })
  }
}

const config = new Chain()

The local requirements are implemented with TS, which is more interesting, just compare the thinking of JS and TS. Let’s break it first. After the question is written in the JS method above, the type will come out, but using TS to fully implement the type also has other uses. Especially in some complex function scenarios, the TS system needs to be used to describe the type. JS really When implementing, the any type is obtained for pure runtime processing, and the type is separated from the runtime.

Ok, let's go back to the topic, let's first write the framework of Chainable :

 type Chainable = {
  option: (key: string, value: any) => any
  get: () => any
}

The question is, how to use the type description option and then connect option or get ? There is more trouble, how to pass the type step by step, let get know what type I am taking at this time?

Chainable must receive a generic type, which defaults to an empty object, so config.get() returns an empty object is also reasonable:

 type Chainable<Result = {}> = {
  option: (key: string, value: any) => any
  get: () => Result
}

The above code is completely fine for the first layer, and the direct call get returns an empty object.

The second step solves the recursion problem:

 // 本题答案
type Chainable<Result = {}> = {
  option: <K extends string, V>(key: K, value: V) => Chainable<Result & {
    [P in K]: V
  }>
  get: () => Result
}

Everyone knows recursive thinking, so I won't go into details. Here is a place that seems not worth mentioning, but it is really easy to deceive people, that is, how to describe an object that contains only one Key value, this value is generic K ?

 // 这是错的,因为描述了一大堆类型
{
  [K] : V
}

// 这也是错的,这个 K 就是字面量 K,而非你希望的类型指代
{
  K: V
}

So the routine description of TS "customary law" [K in keyof T] must be used, even though we know T there is only one fixed type. It can be seen that JS and TS are completely different ways of thinking, so proficient in JS does not necessarily mean proficient in TS, TS still needs a lot of questions to cultivate thinking.

Last of Array

Implementation Last<T> Get the type of the last item of the tuple:

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

type tail1 = Last<arr1> // expected to be 'c'
type tail2 = Last<arr2> // expected to be 1

We have implemented First before, and similarly, here is nothing more than describing the last one as infer during deconstruction:

 // 本题答案
type Last<T> = T extends [...infer Q, infer P] ? P : never

Note here that infer Q someone might write for the first time:

 type Last<T> = T extends [...Others, infer P] ? P : never

It is found that an error is reported, because it is impossible to use an undefined generic type in TS, and if you put Others in Last<T, Others> , you will face a big problem in TS:

 type Last<T, Others extends any[]> = T extends [...Others, infer P] ? P : never

// 必然报错
Last<arr2>

Because Last<arr2> only one parameter is passed in, an error must be reported, but the first parameter is given by the user, and the second parameter is deduced by us. Neither the default value can be used here, nor can it not be written, No solution.

If you really want to write like this, you must use a feature that TS has not yet passed: partial type parameter inference . For example, it is likely that the future syntax is:

 type Last<T, Others extends any[] = infer> = T extends [...Others, infer P] ? P : never

In this way, only one parameter is required for the first pass, and it is also declared that the second parameter is an inferred type. However, this proposal has not yet been supported, and in essence, it has the same meaning and effect as writing infer into the expression, so there is no need to toss for this question.

Pop

Implements Pop<T> and returns the type after removing the last item of the tuple:

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

type re1 = Pop<arr1> // expected to be ['a', 'b', 'c']
type re2 = Pop<arr2> // expected to be [3, 2]

This question is almost the same as Last , just return the first deconstructed value:

 // 本题答案
type Pop<T> = T extends [...infer Q, infer P] ? Q : never

Summarize

It is obvious from the title that there is a big difference between TS thinking and JS thinking. If you want to truly master TS, it is necessary to write a lot of questions.

The discussion address is: Intensive Reading "Get return type, Omit, ReadOnly..." 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 粉丝