TypeScript-Starting from the function return type, how to use TS well

TypeScript-Starting from the function return type, how to use TS well

Preface: For students who are new to TS, the biggest problem is that they are not good at handling the return type of a function. If a function returns multiple types or can be empty, or even composite, or modify the original type, do a combination, and do Pruning, novices often need to define a large number of types with high similarity to solve the problem. Moreover, a single type restriction or use paradigm is often prone to various types of errors. Often students who lack patience will use the any type directly. This goes against the original intention of using TS. It's not even different from using JS. Knowing the techniques of multiple return types and writing the tool-assisted types is the key to good use of TS.

1. Use union type and cross type

For example, we have such a js function

//js code example function fucExp () { if (...condition){ return { "aa" : 123 } } else { return [ 1 , 2 , 3 ] } } Copy code

This fucExp function may return an object or an array. If TS is used, how should the return type be done?

At this time, we should use the union type of TS. Using the | symbol definition, we can first define the union type type, of course, we can also use it directly. The following wording is recommended.

//ts code example type UncertaintyType = Object | number [] function fucExp (): UncertaintyType { if ( 0 < 3 ) { return { "aa" : 123 } } else { return [ 1 , 2 , 3 ] } } Copy code

For example, I have such a function that needs to merge two objects. The returned type is the combined type of the two objects. How do we define the type of TS?

//JS code example function funcMerge () { let dog = { name : "jack" , age : 23 , } let behavior = { bark : () => { console .log( 'wang wang' ) } } return Object .assign(dog, behavior) } Copy code

Again, never use any!!!

We are on the cross type. The cross type combines the members of several types and uses the ampersand to form a new type with all the members of these types. Taken literally, it may be mistaken to take out several types of intersecting (ie intersection) members. (Note: the intersection type is disjoint, it is merged, it is easier to make mistakes here, the key point is to remind) the correct operation is as follows

//TS code example interface Behavior { bark : Function } interface Dog { name : string , age : number } type MergeType = Dog & Behavior function funcMerge (): MergeType { let dog: Dog = { name : "jack" , age : 23 , } let behavior: Behavior = { bark : () => { console .log( 'wang wang' ) } } return Object .assign(dog, behavior) } Copy code

Note: According to the actual return of your function, & and | can be mixed. If the & and | are used too much in a type, it is also very disgusting. For more complex types, please see the detailed explanation of the tool types below.

Two. never type

If we have a function without any return, how to define its return type? You may immediately think of the similar void keyword in JS. When a function returns a null value, its return value is void, but when a function never returns. You cannot use void in TS. For example, our function just throws an error during the execution process.

The never type in TS handles this situation more gracefully. For example, look at the following code example

//TS code example function neverFunc (): never { throw new Error ( 'Throw my error' ); } Copy code

Note: Except for never itself, any other types cannot be assigned to never

3. Type capture

For example, there is a situation where the json data we imported from the outside or the Object imported from other third-party libs does not have a defined type. We need to return its type. What should we do?

In TS, variables can be used in type annotations through the typeof operator. This allows you to tell the compiler that the type of a variable is the same as the other type. The code is as follows

//TS code example let obj = { msg : "no" } function unknowReturnFunc (): typeof obj { //typeof capture object type obj.msg = "ok" return obj } Copy code

Not only can capture the type of the object, but also the type of the member. The code is as follows

//TS code example let obj = { msg : "no" } function unknowReturnFunc (): typeof obj . msg { //typeof capture member type obj.msg = "ok" return obj.msg } Copy code

In another case, we want the type returned by the function to be limited to the specified value range. For example, we have an animal type with dog and cat in it. Then the return type can only be one of these two types, not the other string, so what should be done?

The keyof operator in TS allows you to capture the key of a type, which allows you to easily have types such as string enumerations and constants. Then please see the code below.

//TS code example let animal ={ dog : "dog" , cat : "cat" } type AnimalType = keyof typeof animal function funcKeyOfExp (): AnimalType { let animalObj :AnimalType animalObj= "dog" //ok animalObj= "cat" //ok //animalObj = "bird"//error This type is not allowed to return animalObj } Copy code

As can be seen from the above example, an enum-like type can be easily implemented. In fact, this is the use of the characteristics of the TS literal type. The specific details can be viewed in the official manual. No detailed description here

4. Mixed and multiple inheritance

Class in TS does not support multiple inheritance, and implements in TS can only inherit attributes, not code logic. So how to achieve it. Using the function to return a new class that extends the constructor, you can simulate multiple inheritance with the concept of TS mixing. For example, we have a function that passes in a type and needs to return a new type. This new type is an extension of the old type. This is a mixins operation.

Not much to say, look at the example:

//TS code example type Constructor<T = {}> = new (...args: any []) => T; function userOne < TBase extends Constructor >( Base: TBase ) { return class extends Base { myName = "Felix" } } function userTwo < TBase extends Constructor >( Base: TBase ) { return class extends Base { score = 60 ; updateScore () { this .score = 100 ; } }; } class Person { age = 20 ; } const UserTwo = userTwo(userOne(Person)); const userTwoInstance = new UserTwo() userTwoInstance.myName userTwoInstance.updateScore() userTwoInstance.age //UserTwo This class has all the methods and properties of other classes, multiple inheritance to achieve a clever copy the code

As can be seen from the above example, the function uses paradigm inheritance, returns a new type and cooperates with the mixins operation. You can easily implement a multiple inheritance. This is the concept of TS and hybrid.

5. Make good use of several commonly used tool types

  1. Partial<T>
    Construct optional types

For example, we have a function that needs to return a defined type, but we don't want to return all the fields in the entire type, only some of the fields, but we hope not to modify the existing type. What should I do? At this time, we will use the Partial tool type.

//Partial internal implementation principle type Partial<T> = { [K in keyof T]?: T[K] //Use keyof to add optional attributes to all fields of the type } Copy code

How do we use this Partial? Look at the following example

interface UserInfo { id : number , name : string , mail : string , } function getUserPartail (): Partial < UserInfo > { const userInfo ={ id : 123 } return userInfo //Only the id attribute is initialized, and no error will be reported. If the return type is not Partial, an error will definitely be reported. You can try } Copy code
  1. Require<T>
    Construct a required type return

Of course, there is Partial optional and Require mandatory. 1. let's look at the definition of the Require type

type Require<T> = { [P in keyof T]-?: T[P]; } Copy code

You will find that it is very similar to the Partial type above. With a minus sign in front, this

The role of is to eliminate the optional attributes in the original type attribute, thereby becoming a mandatory attribute. We use Require to modify the above example.

interface UserInfo { id : number , name : string , mail : string , } function getUserPartail (): Partial < Require > { const userInfo = { id : 123 , name : 'felix' , mail : 'xxx@xx.com' } return userInfo //In this way, the type we return must have for every attribute. } Copy code

The role of this Require can be provided to external functions in our calss. We can limit the attributes of certain parameters to be required, and the error can be stuck in the compilation layer, without the need to go at runtime. Reduce the probability of errors in the actual operation of our code.


Pick<T, K>
Select some attributes K from the type T to construct a new type

The Pick type is a very practical type. When the attribute of the returned type has a certain field that we don't want, we can use Pick to remove this attribute instead of making it optional, which can ensure the cleanness and accuracy of the structure. Please see the example below

interface UserInfo { id : number , name : string , mail : string , } //Auxiliary tool is used to remove a property function deleteProperty < T >( obj: T, key:keyof T ): T { const {[key]: deleted, ...newState} = obj; return newState as T } function getUserPick ( userInfo:Pick<UserInfo, 'id' | 'name' > ) { console .log(userInfo)//output {"id": 123,"name": "felix"} return userInfo } getUserPick(deleteProperty({ ID : 123 , name : 'Felix' , mail : 'xxx@xx.com' }, 'mail' )) copying the code

When we pass the entire UserInfo type, it does not meet the requirements and will prompt an error. Because Pick only allows the properties of the id and name fields to be passed, we use the deleteProperty function tool to assist in deleting the mail field. This returns a new type so that no error will be reported. You can run the code yourself, and you will have a clearer understanding.

  1. Record<K, T>
    Construct a type whose attribute name is K and attribute value is T
type Record<K extends keyof any , T> = { [P in K]: T; } Copy code
  1. Exclude<T, U>
    From the type T, remove all attributes that can be assigned to U
of the type Exclude <T, U> = T the extends U? Never : T; Copy the code
  1. Extract<T, U>
    Extract all types that can be assigned to U from type T
of the type Extract <T, U> = T the extends U T:? Never ; Copy the code
  1. Omit<T, K>
    Remove all attributes that can be assigned to K from type T
type the Omit <T, K the extends keyof T> of Pick = <T, Exclude <keyof T, K >> copy the code
interface UserInfo { id : number , name : string , mail : string , } type UserOmit = Omit<UserInfo, 'mail' > function getUserPick (): UserOmit { return { id : 123 , name : "myName" } } Copy code

In fact, this Omit is similar to Pick. Pick is to extract and keep, and Omit is to extract and remove. An opposite concept, but it's easier to understand.

  1. ReturnType<T>
    Construct a type from the return value type of the function type T
type ReturnType<T extends (...arg: any ) => any > = T extends (...arg: any ) => infer R? R: any ; //infer R represents the return value of the function to be inferred. If T is assigned to (... arg: any) => infer R is a result R, otherwise any duplicated code

Six. Conclusion

There are many types of tools, and they can be combined with each other. For example, the Omit type uses a combination of Pick and Exclude. I won t introduce them one by one here. The master leads in, and the practice depends on the individual. Of course, if you encounter a very useful type of tool, I will continue to add. Of course, GITHUB has a lot of useful tool type libraries. I personally recommend ts-toolbelt and utility-types to learn more about them. Only by mastering these types of skills can you write more Professional TS codes.