The end of 2018, force released the staffing, including front-end, warehouse address: https://github.com/LeetCode-OpenSource/hire. Unlike most JD’s, it offers five questions and notes: Complete one or more interview questions to get an interview without the first round. The more questions completed, the higher the quality, the more points in the interview. The finished code can be sent to [email protected] in any form. Completion of one or more of these questions may lead to an interview, depending on the code submitted to us.


(JD, Chinese front-end engineer)

Today we look at problem two: writing complex TypeScript types. What level does TypeScript have to go to in order to make a connection to the current end?

The other four questions are also interesting and worth a look.

Problem description

Suppose you have a class called EffectModule

class EffectModule {}
Copy the code

Methods on this object “can only” have two types of signature:

interface Action<T> {
payload? : T  type: string
}

asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>  syncMethod<T, U>(action: Action<T>): Action<U> Copy the code

The object may also have some arbitrary “non-function attributes” :

interface Action<T> {
payload? : T;  type: string;
}

class EffectModule {  count = 1;  message = "hello!";   delay(input: Promise<number>) {  return input.then((i) = > ({  payload: `hello ${i}! `. type: "delay". }));  }   setMessage(action: Action<Date>) {  return { payload: action.payload! .getMilliseconds(), type: "set-message". };  } } Copy the code

Now there is a function called connect that takes an instance of EffectModule and turns it into another object with only the “method of the same name” on it, but the method’s type signature has been changed:

asyncMethod<T, U>(input: Promise<T>): PromiseThe < Action < U > > turned out to beasyncMethod<T, U>(input: T): Action<U>
Copy the code
SyncMethod <T, U>(Action: action <T>): Action<U> becomessyncMethod<T, U>(action: T): Action<U>
Copy the code

Example:

EffectModule is defined as follows:

interface Action<T> {
payload? : T;  type: string;
}

class EffectModule {  count = 1;  message = "hello!";   delay(input: Promise<number>) {  return input.then((i) = > ({  payload: `hello ${i}! `. type: "delay". }));  }   setMessage(action: Action<Date>) {  return { payload: action.payload! .getMilliseconds(), type: "set-message". };  } } Copy the code

After connect:

type Connected = {
  delay(input: number): Action<string>;
  setMessage(action: Date): Action<number>;
};
const effectModule = new EffectModule();
const connected: Connected = connect(effectModule); Copy the code

Requirements:

In the index.ts file in the title link [1], there is a type Connect = (module: The connected type in index.ts is the same as:

type Connected = {
  delay(input: number): Action<string>;
  setMessage(action: Date): Action<number>;
};
Copy the code

“Perfect match.”

The above is the official title description, I add below

Index. ts = index.ts = index.ts


(Topic Additional information)

Train of thought

So let’s read the title first. We are asked to supplement the definition of the Connect type by replacing any with some other code that does not report an error.

Review the title information:

  • There is a callconnectFunction that takes an instance of EffectModule and turns it into another object on which onlyEffectModule method with the same name, but the method’s type signature has been changed
  • There may also be some arbitrary non-function properties on this object
  • Methods on this object (an EffectModule instance) are “only possible” to have two type signatures

Based on the above information, we can conclude that we just need to change the signature of the function type on the instance of EffectModule passed as a parameter and remove the non-function attribute. So, we have two problems to solve:

  1. How do I get rid of non-function attributes
  2. How do I convert function type signatures

How do I get rid of non-function attributes

We need to define a generic that takes an object and keeps it if its value is a function. Otherwise, we can drop it. For those of you who don’t know about generics, take a look at my previous post on TypeScript Generics you Don’t Know about [2]

I’m reminded of the official offer of a general Omit Omit. Here’s an example:

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}
 type TodoPreview = Omit<Todo, "description">;  // The description attribute is missing const todo: TodoPreview = {  title: "Clean room". completed: false.}; Copy the code

Official Omit:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};
type Exclude<T, U> = T extends U ? never : T;
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Copy the code

In fact, what we need to do is a variation of Omit, not some keys, but non-functional keys.

Since Omit functions are really just Pick functions and don’t need to explicitly specify keys, our generics accept only one argument. So I copied the official Pick and wrote the following code:

/ / get the value for the function key, like: 'funcKeyA' | 'funcKeyB'
type PickFuncKeys<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

// Get a key value pair of functions, such as {'funcKeyA':... , 'funKeyB': ... } type PickFunc<T> = Pick<T, PickFuncKeys<T>>; Copy the code

Effect:

interface Todo {
  title: string;
  description: string;
  addTodo(): string;
}
 type AddTodo = PickFunc<Todo>;  const todo: AddTodo = {  addTodo() {  return "Focus on the front end of imagination.";  }, };  type ADDTodoKey = PickFuncKeys<Todo>; // 'addTodo' Copy the code

As you can see, PickFunc only extracts function attributes and ignores non-function attributes.

How do I convert function type signatures

Let’s review the requirements:


That is, we need to know “how to extract values from Promise and Action generics.”

In fact, the two are almost the same. If you know one, you know the other. Let’s start with Promise.

From:

(arg: Promise<T>) => Promise<U>
Copy the code

To:

(arg: T) => U;
Copy the code

To accomplish this requirement, infer is needed. Simply add a keyword prefix to the type and TS will automatically populate the derived type.

Infer, first seen in this official PR, represents type variables to be inferred in the extends condition statement.

A simple example is as follows:

type ParamType<T> = T extends (param: infer P) => any ? P : T;
Copy the code

The conditional statement T extends (param: infer P) => any? In P: T, infer P means function parameters to be inferred.

Infer P => ANY If T can be assigned to (Param: infer P) => any, the result is (Param: infer P) => any, otherwise T is returned.

A more specific example:

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

type Func = (user: User) = > void;  type Param = ParamType<Func>; // Param = User type AA = ParamType<string>; // string Copy the code

This knowledge is enough for us. More usage can be found at typescript-infer [3].

Based on the above knowledge, it is not difficult to write the following code:

type ExtractPromise<P> = {
  [K in PickFuncKeys<P>]: P[K] extends (
    arg: Promise<infer T>
) = >Promise<infer U>
    ? (arg: T) = > U
 : never; }; Copy the code

The code for extracting the Action is similar:

type ExtractAction<P> = {
  [K in keyof PickFunc<P>]: P[K] extends (
    arg: Action<infer T>
  ) => Action<infer U>
    ? (arg: T) = > Action<U>
 : never; }; Copy the code

We have solved both problems at this point, and see the code section below for the complete code.

The key point

  • The generic
  • Extends does type constraints
  • Infer does type extraction
  • Use and implementation of built-in basic paradigms

code

If we string the dots together, it’s not hard to write the following final code:

type ExtractContainer<P> =  {
  [K in PickFuncKeys<P>]:
    P[K] extends (arg: Promise<infer T>) => Promise<infer U> ? (arg: T) = > U :
      P[K] extends (arg: Action<infer T>) => Action<infer U> ? (arg: T) = > Action<U> :
        never
type Connect = (module: EffectModule) = > ExtractContainer<EffectModule> Copy the code

The complete code is on my Gist[4].

conclusion

We first define the problem and then break it down into: 1. How to remove non-function attributes, 2. How to convert function type signatures. Finally, we start with the decomposition problem, and the basic generics tools, and the syntax that might be used.

The problem is not difficult, medium at best. But as you can probably see, it’s not just a grammar and API test, it’s a general strength test. This is particularly true for the other four questions. This way of investigation can really examine a person’s comprehensive strength, back is not back. I personally love asking this question when INTERVIEWING people.

Only “master the basic + problem-solving thinking method”, in the face of complex problems can be leisurely, easy.

You can also follow my public account “Imagination Front” to get more and fresher front-end hardcore articles, introducing you to the front end you don’t know.


Lucifer – Zhihu [5]

Pay attention, don’t get lost!

Reference

[1]

Title link: https://codesandbox.io/s/4tmtp


[2]

You don’t know the TypeScript of generics (word long, recommend collection) : https://lucifer.ren/blog/2020/06/16/ts-generics/


[3]

Deep understanding of TypeScript – infer: https://jkchao.github.io/typescript-book-chinese/tips/infer.html#%E4%BB%8B%E7%BB%8D


[4]

Gist address: https://gist.github.com/azl397985856/5aecb2e221dc1b9b15af34680acb6ccf


[5]

Lucifer – zhihu: https://www.zhihu.com/people/lu-xiao-13-70