I recently took time to review a wave of technical knowledge about Typescript as I prepared to roll out the project. I say review because I’ve seen it before and I’ve forgotten all about it. In addition to not being used often, Typescript knowledge is relatively fragmented and hard to learn in a systematic way, so take the opportunity to organize it and use it as a study note.

preface

Typescript (TS for short) is a superset of Javascript that provides a comprehensive set of syntactical definitions and usage constraints that enhance the readability and maintainability of JS code, most importantly the TS type system. TS type system provides a complete set of schemes for declaring and using types, which is flexible but complex and difficult to remember, resulting in a high cost of LEARNING TS.

When learning about the TS type system, it is important to remember that the type system only works at compile time and will not end up in your business code. It is important to realize this, because it is easy to learn TS and JS syntax code together to understand, in fact, this is not true, TS type system is only a TS to JS reinforcement, learning TS type system, the first is to forget JS, The type system should be separated from JS as far as possible to avoid some interference in the understanding of syntax.

Making types more flexible and generic is the goal of all syntactic features in a type system. Understanding the TS type system independently is also based on this goal from shallow to deep.

Let’s start with the simplest type

Declaring type is the premise of using type. TS provides many syntax features for declaring type. Its sole purpose is to improve and strengthen TS’s ability to declare type, making type declaration more flexible and reusable.

Let’s start with the simplest types, both basic and advanced. The easiest way to declare a type in TS is to use the type keyword.

The base type

Basic types correspond to multiple basic data types of JS, including:

  • Original type:boolean,number,string,void,any,null,undefinedAs well assymbol.
  • Reference type: objectObject, functions,FunctionAnd an array ofArrayAnd so on.

This is the simplest data type in TS. Let’s list a few of the most common ones:

// When declaring:
type _string = string;  // A string of characters
type _number = number;  // Number type
type _boolean = boolean;  // Boolean type
type _any = any;  / / arbitrary value
type _object = {  // Object type
  name: string;
  age: number
}
type _array = _object[];  // Array type
type _function = (user: _object) = >number;  // Function type

// When used:
const str: _string = '111';
const num: _number = 3;
const show: _boolean = true;
let err: _any = - 1;
err = 'something wrong';
const user: _object = {
  name: 'hankle',
  age: 23
}
const list: _array = [user];
const getAge: _function = (user) = >{
  return user.age
}
Copy the code

We impose a layer of type constraint on our variables by declaring several different types and using them as: [type]. We can also see from the above example that type can declare an alias for an existing type.

High-level types

Advanced types, which give us more flexibility to reuse existing types, are an effective means of reuse.

The joint type

Joint type said value can be one of many types, using | connector, mechanism of action is similar to “or”.

type keyType = string | number;

const strKey: keyType = 'key';
const numKey: keyType = 1;
Copy the code

The key type can be string or number.

Cross type

A cross type means that a variable should have all the properties of multiple types. It uses the ampersand (&), which acts like a “union”, mostly for object types. When we need to extend some already defined base object types, we can declare them using cross types.

type User1 = {
  name: string;
  age: number
};
type User2 = {
  sex: number
}

const user: User1 & User2 = {
  name: 'hankle',
  age: 23,
  sex: 0
};
Copy the code

_user1&_user2 indicates that the variable needs to have all the attributes of _User1 and _User2.

Literal type

The type declared by type can also be a literal, indicating that the type can only take on the same fixed value. This is called a literal type:

type coco = 'coco';
const str: coco = 'coco coco';
// Cannot assign type "coco coco" to type "coco".
Copy the code

There is a common literal type called string literal type, which is used in combination with union types to restrict the value to one of several strings:

type Key = 'name' | 'age';
type User = {
  name: string,
  age: number
}

const user: User = { name: 'hankle', age: 23 }
const getVal = (user: User, key: Key): any= > user[key]

getVal(user, 'sex');
// Arguments of type "sex" cannot be assigned to arguments of type "Key".
Copy the code

The key type limits the attribute keys that can be used for user objects, preventing code from trying to obtain attributes that do not exist in the user.

Interface: Another way to declare a type

In addition to type, there is another way to declare types in TS, and that is interfaces. Use the interface keyword:

Type User = {name: string; age: number } */
interface User {
  name: string;
  age: number
}

Type List = User[] */
interface List {
  [index: number]: User
}

Type findFunc = (list: list, name: string) => User */
interface findFunc {
  (list: List, name: string): User
}
Copy the code

Why create an “interface” to declare a type when you already have one? In fact, in many OOP languages such as Java, interfaces are an important concept for abstracting class behavior. Thus, TS interfaces can describe the shapes of classes in addition to object types, array types, or function types. Through implements and extends, types defined by interfaces can be highly reusable, which is the original purpose of the TS type system.

In TS, class interfaces are declared and implemented like this:

// Define an alert class interface that requires all classes that implement it to have an alert method
interface Alarm {
    alert();
}
// The SecurityDoor class implements this interface and the Alert method
class SecurityDoor implements Alarm {
    alert() {
        console.log('SecurityDoor alert'); }}// Car does not implement the alert method
class Car implements Alarm {
}
// Class "Car" error implements interface "Alarm".
// Property 'alert' is missing in type 'Car' but required in type 'Alarm'.
Copy the code

Types declared by the type keyword can be extended by combining types, while interfaces are implemented using inheritance:

interface User1 {
  name: string;
  age: number
};
// Declare interface User2, inherited from interface User1, which needs to contain the shape of User1
interface User2 extends User1 {
  sex: number
}

const userq: User2 = {
  name: 'hankle',
  sex: 0
};
// Property 'age' is missing in type '{ name: string; sex: number; }' but required in type 'User2'.
Copy the code

As for when to use type and when to use interface, there are a lot of interpretations on the web. In fact, these two things can do are almost the same, as for which to choose, I think in the peacetime business scene development, if more is to some function structure, like structure, class structure type constraints, the use of interface will be more semantic, it is recommended to use interface to declare the type.

Generics: Make your types more generic

If we had a find function equivalent to array. find and needed to declare its own function type, we would probably have to use the declaration overload of the function so far:

/ / declare overloaded function find (list: string [], func: (item: string) = > Boolean) : string | null; function find(list: number[], func: (item: number) => boolean): number | null; function find(list: object[], func: (item: object) => boolean): object | null; Function find(list, func) {return list.find(func)}Copy the code

Function overloading allows for joint types of functions by declaring different shapes of functions multiple times. The find method allows multiple types of arrays to be passed in, and its return value is determined by the type of element contained in the passed array. As can be seen from the above, we repeatedly declare only different element types, function parameter structure is not changed, so there is a lot of repeated code.

Function generics

Generics are designed to solve this problem. As the name implies, generics represent indeterminate types, allowing complex types to retain indeterminate types internally when declared and then specified when used. A generic type is equivalent to a function, specifically a factory function of a type. Since it is a function, it is required to “pass parameters”, which is implemented using <>. The above example with generics looks like this:

Function find<T>(list: T[], func: (item: T) => Boolean): function find<T>(list: T[], func: (item: T) => Boolean) T | null {return list. The find (func)} / / to find specific type when using the < number > ([1, 2, 3], the item = > item > 2)Copy the code

Of course, you can also declare function types using interfaces:

// Find's type, FindType, is a generic
interface FindType {
  <T>(list: T[], func: (item: T) = >boolean) :T | null
}
const find: FindType = function (list, func) {
  return list.find(func} // Specify a specific type when usedfind<number> ([1,2,3], item => item > 2)
Copy the code

Generics also allow you to specify multiple type parameters:

// The tuple element is swapped
function swap<T.U> (tuple: [T, U]) :U.T] {
  return [tuple[1], tuple[0]];
}

swap<number.string> ([7.'seven']); // ['seven', 7]
Copy the code

A generic class

Generics apply not only to function types, but also to class type definitions in general. Similar to specifying the type on a function call, the generics of a class are specified when the object is instantiated:

// Declare a generic class
class EleList<T> {
  list: T[]
  add: (item: T) = > number
}

// Specify a string type when instantiated
const users = new EleList<string> (); users.list = ['hankle'.'nancy'];
users.add = function (item) {
  this.list.push(item);
  return this.list.length;
}
Copy the code

Generic constraint

Generics allow us to preset and use an indeterminate type, but sometimes “indeterminacy” can affect our use. For example, we have a utility method that fetches a length. The input can be an array or an array-like object:

function get<T> (arg: T) :number {
  return arg.length; // Attribute "length" does not exist on type "T".
}
Copy the code

However, we found that it was wrong. The type T is arbitrary in this case, and we cannot guarantee that all types specified when used will have the attribute Length. We want T to be limited in that it can only be an array or array-like object, or more specifically, it must contain the Length attribute. So this is where generic constraints come in handy:

// Declare the LengthType type, which must include the length attribute
interface LengthType {
  length: number;
}

// T inherits from the LengthType type, which means it must have the length attribute
function get<T extends LengthType> (arg: T) :number {
  return arg.length;
}
Copy the code

Here we declare a type with a length attribute, and then let T inherit that type to constrain the structure of the generic. We said that interface inheritance also uses the extends keyword. The underlying meaning of extends is that the inheritor must contain all the properties of the inheritor, so generic constraints also use extends to limit the structural shape of generics. Generic constraints can help us eliminate a lot of meaningless judgment logic. In real development, our generics are often not completely arbitrary, so we should be good at using generic constraints.

Higher-order types: Generics can also be played this way

Generics can be the most rewarding feature of the TS type system. As mentioned earlier, generics are a type factory function. In other words, generics can be used to further build a wide variety of types. TS officially refers to these types as advanced types as well, but I think it’s more appropriate to call them advanced types.

Mapping type

Mapping types are a common generic structure. A mapping type can map attributes of an old type to generate a new type. The TS library already has several very useful mapping types built in, including Partial, Required, Readonly, Pick, and Record, to explain a few.

Partial<T>

type Partial<T> = {
    [P inkeyof T]? : T[P]; };Copy the code

Keyof returns a string literal of the key names of all the attributes in T, so Partial is used to convert attributes in T to optional attributes.

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

// Attributes of type User are mandatory and attributes of type PartialUser are optional
type PartialUser = Partial<User>
/** * type PartialUser = { * name? : string; * age? : number; * sex? : number; *} * /
Copy the code

Required<T>

type Required<T> = {
    [P inkeyof T]-? : T[P]; };Copy the code

In contrast to Partial, Required is used to convert attributes in the incoming type T to Required attributes.

interfaceUser { name? :string; age? :number; sex? :number;
};

// Properties of type User are optional, and RequiredUser properties are required
type RequiredUser = Required<User>
/** * type RequiredUser = { * name: string; * age: number; * sex: number; *} * /
Copy the code

Readonly<T>

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
Copy the code

Readonly is used to convert all attributes in the passed type T to read-only attributes.

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

// Attributes of type User are writable, and attributes of type ReadonlyUser are read-only
type ReadonlyUser = Readonly<User>
/** * type ReadonlyUser = { * readonly name: string; * readonly age: number; * readonly sex: number; *} * /
Copy the code

Pick<T, K>

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
Copy the code

Pick is used to generate a new type with specified attributes based on the incoming type, that is, we can Pick some attributes from the old type to form a new type.

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

// The PickUser type has no sex attribute
type PickUser = Pick<User, 'name' | 'age'>
/** * type PickUser = { * name: string; * age: number; *} * /
Copy the code

Record<T, K>

type Record<K extends keyof any, T> = {
    [P in K]: T;
};
Copy the code

Record generates an object type from a string literal type, the entries of which are the attribute keys of the object type. Unlike the previous types, Record is not homomorphic, that is, it does not require an input type to copy properties, but instead creates new properties directly.

type RecordUser = Record<'name' | 'telephone' | 'description'.string>
/** * type RecordUser = { * name: string; * telephone: string; * description: string; *} * /
Copy the code

Conditions in the

Another common generic construct is the conditional type. Conditional types can provide conditions for combinational conversions of types when a new type is generated. Also, the TS standard library has some common condition types built in, including Exclude, Extract, ReturnType, and Parameters.

Extract<T, U>

type Extract<T, U> = T extends U ? T : never
Copy the code

Extract

Extract

Extract

Extract

Extract

Extract

Extract

Extract

Extract

Extract
,>
,>
,>
,>
,>
,>
,>
,>
,>
,>

type UserKey = Extract<'name' | 'age' | 'sex'.'name' | 'sex' | 'descrption'>
// type UserKey = "name" | "sex"
Copy the code

Exclude<T, U>

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

Exclude

is used to Exclude members of the union type U from the union type T and generate a new union type. That is, the generated union type contains only those members that T has and U does not. It is similar to finding non-sets.
,>

type UserKey = Exclude<'name' | 'age' | 'sex'.'sex'>
// type UserKey = "name" | "age"
Copy the code

Using Exclude, we can also implement the opposite of NeverPick:

type NeverPick<T, U> = {
  [P in Exclude<keyof T, U>]: T[P];
};

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

type NeverPickUser = NeverPick<User, 'sex'>
/** * type NeverPickUser = { * name: string; * age: number; *} * /
Copy the code

Parameters<T>

type Parameters<T extends(... args:any) = >any> = T extends(... args: infer P) =>any ? P : never
Copy the code

Returns the type of the input parameter to a function when the specified type is passed in. The infer operator is used to represent the type to be inferred.

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

// Define a function type GetUserFunc with an input parameter name of type string
interface GetUserFunc {
  (name: string): User 
}

// The parameter type of GetUserFunc is extracted
type funcParamsTypes = Parameters<GetUserFunc>
// type funcParamsTypes = [string]
Copy the code

ReturnType<T>

type ReturnType<T extends(... args:any) = >any> = T extends(... args:any) => infer R ? R : any
Copy the code

ReturnType Returns the type of the value returned by the function when the specified type is passed in.

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

// Define a function type GetUserFunc that returns a value of type User
interface GetUserFunc {
  (name: string): User 
}

// The parameter type of GetUserFunc is extracted
type funcReturnTypes = ReturnType<GetUserFunc>
// type funcReturnTypes = User
Copy the code

Refer to the article

Typescript Official Documentation – Advanced Types explain generics in Typescript and inferences in conditional types


If you think this article is valuable to you, please like it and follow us on our official website and WecTeam. We have excellent articles every week