preface

I have been working on TypeScript development projects for the past three months. At first, I felt it was unnecessary and time-consuming. Fortunately, I have one advantage: I am willing to work hard and learn whether it is useful or not

Lately, we’ve seen TypeScript become a trend, almost standard on large projects. TypeScript makes up for some of the weaknesses of weakly-typed JavaScript by helping us build more robust code while also making it more readable and maintainable. Many run-time errors can be exposed at compile time, making potential problems easier to find.

Generics and related features, such as conditional inference and so on, are some of the more difficult and time-consuming aspects of TypeScript. This article summarizes and shares with you some relevant knowledge ~

The generic

The purpose of generics in TypeScript is to provide meaningful constraints between members, add a layer of abstraction to your code, and increase reusability. Generics can be applied to Typescript functions (function parameters, function return values), interfaces, and classes (instance members of classes, methods of classes).

A simple example

So let’s start with this. If we were to write a function’s argument and return value type, we would probably write ~ and we’re constrained that the function’s argument and return value must be numeric.

function identity(arg: number) :number {
  return arg;
}
Copy the code

So here’s the question. If I want to restrict the argument and return value types to strings, I’ll write it this way.

function identity(arg: string) :string {
  return arg;
}
Copy the code

Unscientific! This becomes very inflexible when functions want to support multiple types of arguments or return values. And that’s where generics come in!

Consider the following:

function identity<T> (arg: T) :T {
  return arg;
}
Copy the code
function identities<T.U> (arg1: T, arg2: U) :T.U] {
  return [arg1, arg2];
}
Copy the code

With generics, you can accept any type, but you complete the constraint between function parameters and return values. Very flexible ~ reusability greatly enhanced!

Generic constraint

Sometimes we define generics that don’t want to be too flexible or that we want to inherit classes and so on. We can use extends to constrain generics.

interface ILengthwise {
  length: number;
}

function loggingIdentity<T extends ILengthwise> (arg: T) :T {
  console.log(arg.length);
  return arg;
}
Copy the code

The React component uses generics to ensure that its Props and State are type-safe

interface ICustomToolProps {
  // @TODO
}

interface ICustomToolState {
  // @TODO
}

class CustomTool extends React.Component<ICustomToolProps, ICustomToolState> {
  // @TODO
}
Copy the code

So you can see that ICustomToolProps and ICustomToolState are also generic. Examples of generic syntax simplifications applied to classes are as follows:

class Directive<T> {
  private name: T;
  public getName(): T {
    return this.name;
  }
  // @TODO
}
Copy the code

When generics are used, T, U, and V are generally used. If they are complex, better semantic descriptions should be used, such as the example of the React component.

Practice a

For example, an instruction manager object is designed to manage instructions

enum EDirective {
  Walk = 1,
  Jump = 2,
  Smile = 3
}
class DirectiveManager<T> {
  private directives: Array<T> = [];
  add = (directive: T): Array<T> => {
    this.directives = this.directives.concat(directive);
    return this.directives;
  };
  get = (index: number) :T= > {
    return this.directives[index];
  };
  shift = (): Array<T> => {
    this.directives = this.directives.slice(1);
    return this.directives;
  };
  // @TODO
}
Copy the code

Initialize an instance of an instruction manager. Given a generic type of number.

You can see that the directive manager object is successfully typed, and TypeScript will alert you if the parameter type is passed wrong.

Learn about generics for array methods

After the above introduction, I believe that we all have a certain understanding of generics! So let’s take a look at generics for JavaScript array methods

Let’s take a look at the following array object properties and method generics.

interface Array<T> {
  length: number;
  [n: number]: T; reverse(): T[]; shift(): T; pop(): T; unshift(... items: T[]):number; push(... items: T[]):number; slice(start? :number, end? :number): T[]; sort(compareFn? :(a: T, b: T) = > number): T[]; indexOf(searchElement: T, fromIndex? :number) :number; lastIndexOf(searchElement: T, fromIndex? :number) :number;
  every(callbackfn: (value: T, index: number, array: T[]) = > boolean, thisArg? :any) :boolean;
  some(callbackfn: (value: T, index: number, array: T[]) = > boolean, thisArg? :any) :boolean;
  forEach(callbackfn: (value: T, index: number, array: T[]) = > void, thisArg? :any) :void;
  map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg? :any) :U[];
  filter(callbackfn: (value: T, index: number, array: T[]) = >boolean, thisArg? :any) :T[];

  splice(start: number) :T[];
  splice(start: number, deleteCount: number. items: T[]) :T[];

  concat<U extends T[] > (. items: U[]) :T[];
  concat(. items: T[]) :T[];

  reduce(
    callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue? : T) :T;
  reduce<U> (
    callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U,
    initialValue: U
  ) :U;

  reduceRight(
    callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue? : T) :T;
  reduceRight<U> (
    callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U,
    initialValue: U
  ) :U;
}
Copy the code

I’m sure you’re all familiar with array methods ~ I’ll take a look at some of them

shift/pop & push/unshift

shift(): T; pop(): T; unshift(... items: T[]):number; push(... items: T[]):number;
Copy the code

There are a couple of ways you might get confused. But after looking at their function signatures, does it feel at a glance? The numeric type returned from the push/unshift method call is the length of its array. The shift/ POP method returns the pop-up element,

forEach & map

forEach(callbackfn: (value: T, index: number, array: T[]) = > void, thisArg? :any) :void;
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg? :any) :U[];
Copy the code

These two methods are worth saying, because both have the characteristics of traversal, so common many students mix these two methods, in fact, a lot of attention. See that forEach’s method actually returns void, and in the map method, it ends up mapping T[] to U[]. So, in a nutshell, the side effects that forEach typically uses to perform, such as making persistent changes to elements, arrays, states, and so on, and printing logs, are inherently impure. The map method, used as a mapping of values, is essentially pure and important in functional programming.

concat

Splice, concat, Reduce and reduceRight are basically overloaded twice, which obviously tells us that these methods have multiple ways of parameter passing and invocation.

For example concat(… items: U[]): T[]; This uses the generic constraint described above, which means that multiple array elements can be passed. The concat(… items: T[]): T[]; That tells us that we can pass multiple elements. Both function signatures tell us that the function returns an array of elements from the called object, each argument in the order of the elements of the argument (if the argument is an array) or the argument itself (if the argument is not an array). It does not recurse into nested array parameters.

Mapping type

Sometimes we have a scenario where we need to create new types from old types, and TypeScript provides a way to map types. In a mapping type, the new type converts every attribute of the old type in the same way

For example, let’s make each property of type readonly, as shown in the figure

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

Similarly, see the figure for understanding ~

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

So you should also get the intent of the following code

type Nullable<T> = { [P in keyof T]: T[P] | null };
Copy the code

The extension allows you to write any mapping type to suit your own needs

enum EDirective {
  Walk = 1,
  Jump = 2,
  Smile = 3
}
type DirectiveKeys = keyof typeof EDirective;
type Flags = { [K in DirectiveKeys]: boolean };
Copy the code

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

Inference in condition types

Infer represents the type variables to be inferred in the extends condition statement.

In the extends statement of a conditional type, we can infer a type variable and then use that type variable in its branch statements. If you do not understand, it does not matter, please continue to see the following example ~

Extract function parameters & extract function return values

In this statement (Param: infer P), a type variable P is declared for the first parameter of the function. If the generic type T is a function, the function parameters inferred from the previous type variable P are extracted and returned; otherwise, the original type is returned.

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

As shown in the figure, the parameter type of IPrint is successfully extracted.

Similarly, the return value of extraction is also understood as ~

type ReturnType<T> = T extends(... args:any[]) => infer P ? P : any;
Copy the code

Extract constructor parameter type & extract instance type

The following code can extract the constructor argument type ~

type ConstructorParameters<T extends new(... args:any[]) = >any> = T extends new (
  ...args: infer P
) => any
  ? P
  : never;
Copy the code

T extends new (… Args: any[]) => any Args: Infer P) declares parameter inference as type variable P. The rest is still the same understanding ~

The following extract instance types are different from the extract constructor parameter types.

type InstanceType<T extends new(... args:any[]) = >any> = T extends new(... args:any[]) => infer R
  ? R
  : any;
Copy the code

Other commonly used conditional inference

The rest of the list some more practical, referring to the above way to understand, students if interested, can Google ~

Extracts the array child element

type Flatten<T> = T extends (infer U)[] ? U : T;
Copy the code

Extract the Promise value

type Unpromisify<T> = T extends Promise<infer R> ? R : T;
Copy the code

Turn the Tuple Union

type ElementOf<T> = T extends Array<infer E> ? E : never;
Copy the code

Turn the Union Intersection computes

type UnionToIntersection<U> = (U extends any ? (k: U) = >void : never) extends ((
  k: infer I
) = >void)?I
  : never;
Copy the code

prompt

Generics are removed at compile time, so do not write Typeof T, new T, instanceof T in generic functions.

When to use generics?

  1. When functions, interfaces, and classes accept multiple types of arguments, generics can be used to increase reusability.
  2. When a function, interface, or class needs a type in more than one place.

summary

In general, using TypeScript in a medium – to large-sized project is still valuable for now, as it puts more constraints on development and significantly reduces runtime errors. I recommend learning more about TypeScript to write cleaner, more robust code.

This article introduces generics and conditional inference to make writing TypeScript types more flexible and extensible

Above ~ thank you for reading, if helpful to you, I feel honored ~

The resources

  • Typescript Advanced Types
  • TypeScript tests/lib/lib.d.ts
  • Distributive conditional types
  • Type inference in conditional types
  • Typescript Generics Explained