SoFunction
Updated on 2025-02-28

How to explain TypeScript generics in a popular way

Overview

In TypeScript, we use generics to constrain the related types of the function. The function here also contains the class constructor, so generics can also be used for declaration part of a class. So, what exactly is a generic? What if generics are understood in a popular way?

What is a generic

Generics refers to a feature that does not specify a specific type in advance when defining a function, interface or class, but then specifies a type when used.

In a popular way, generics are "parameters" in the type system, and their main function is to reuse types. As can be seen from the above definition, it will only be used in functions, interfaces, and classes. It is a function parameter in a js program that is two levels of things (although the meaning is the same), because typescript is a static type system and a system that performs type checking when js is compiled. Therefore, generic parameters are actually used at runtime during the compilation process. The reason why it is called "parameter" is because it has the same characteristics as function parameters.

function increse(param) {
  // ...
}

In the type system, we use generics like this:

function increase<T>(param: T): T {
  //...
}

When param is a type, T is assigned to this type. In the return value, T is the type to perform type checking.

Compile system

You should know that typescript itself also needs to be programmed, but its programming method is very strange. You need to intersperse js code in its program code (the saying of interspersing js code in the ts code is very strange, because our intuitive feeling is that ts code is mixed in the js code).

In programming, the most important form is functions. In typescript type programming, do you see functions? No. This is because where there are generics, there are functions, but the form of the function is broken by the js code. typescript needs to be compiled to get the final product. There are two things to do during the compilation process. One is to run type programming code in memory to form a type checking system. That is to say, we can type check the js code. First, the typescript compiler runs the ts programming code and gets a runtime checking system. This article comes from Fuzige's podcast. Runs this system to make type assertions on the js code interspersed therein; the other is to output js. During the output process, the compilation system has completed the type programming code, just like the echo js code in the php code. The php code has been run, and the js code is displayed.

Looking at typescript from this perspective, you may be able to better understand why it is said to be a superset of JavaScript and why its compilation result is js.

Common understanding of generics

Since we understand the logic of the ts compilation system, we can emotionally distinguish type programming from business programming of js itself. The "generics" we are talking about only exist in the type programming part, which is the compiled runtime code of ts.

Let's look at the next simple example:

function increase<T>(param: T): T {
  //...
}

What would happen if we distinguish js code from this code and then use type description text to represent it?

// Declare the function @type, the parameter is T, the return result is (T): T@type = T =&gt; (T): T

// Run the function to get a type F, that is, type (number): number@F = @type(number)

// Requires increase function to comply with the type F, that is, the parameter is number, and the return value is also number@@F
function increase(param) { 
  // ... 
} 
@@end

Actually, there is no such syntax as @@F, which I made up, and the purpose is to allow you to look at the type system from another perspective.

When we understand that generics are "parameters", we may ask: Where are the functions of the type system? For js functions, you can easily point out the function declaration statements and parameters, but in ts, this part is hidden. However, we can easily see the shadow of type functions in some specific structures:

// Declare a generic interface. This is written like declaring a function. We use descriptive language to describe @type = T => (T): Tinterface GenericIdentityFn&lt;T&gt; {
    (arg: T): T;
}

// This is written like a closure function. After declaring the function, run the function immediately. The description language is: @@[T => (T): T](any)function identity&lt;T&gt;(arg: T): T {
    return arg;
}

// Using a generic interface is like calling a function. We use a description language to describe @type(number)let myIdentity: GenericIdentityFn&lt;number&gt; = identity;

We will rewrite the entire code above with the description text:

@GenericIdentityFn = T => (T): T

@@[T => (T): T](any)
function identify(arg) {
  return arg
}
@@end

@@GenericIdentityFn(number)
let myIdentity = identity
@@end

We declare two functions in the type system, namely @GenericIdentityFn and @some (anonymous function @[T => (T): T]). Although they are two functions, in fact, theirs are exactly the same, because typescript is a structure type, that is, when type checking, only determines whether each node type on the structure is the same, rather than keeping the pointer of the type variable itself the same. The two functions @GenericIdentityFn and @some are called respectively to modify identify and myIdentify. When called, the received parameters are different, so the final type checking rules are different. Identify only needs to ensure that the types of parameters and return values ​​are the same. As for the specific type, any. In addition to ensuring that the return value of the parameter is the same, myIdentify also requires that the type must be number.

Generic Classes

In addition to generic interfaces, class classes can also be genericized, that is, "generic classes". With the help of generic classes, we will explore the steps of declaration and use of generics.

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();

The generic interface in the previous article is written very much like a function because it is just to constrain the type of a function. In fact, we can redescribe a generic interface and a generic class in a descriptive language. The red part above is described in descriptive language:

@GenericNumber = T => class {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

The @GenericNumber function takes T as the parameter and returns a class. The parameter T is used many times in the @type function body.

@GenericIdentityFn = T => interface { 
  (arg: T): T; 
}

We redescribe the previous interface GenericIdentityFn so that we can add other methods to the interface.

It can be noted that even after the basic types built into typescript, such as Array, are declared as generic interfaces and generic classes, these interfaces and classes must be passed in the <> when used, essentially because they are all functions, but the return values ​​are different.

Common explanation of other generic use

Next we will describe a complex type:

class Animal {
    numLegs: number;
}

function createInstance<A extends Animal>(c: new () => A): A {
    return new c();
}

Let’s not look at the new() part, we look at the extends syntax in angle brackets. How should we understand it here? Actually, the question we are facing is when at compile time, when does the content in the angle brackets in <A extends Animal> run, before, or between?

// In the end it is@type = (A extends Animal) =&gt; (new() =&gt; A): A
@type(T)
// still@type = A =&gt; (new() =&gt; A): A
@type(T extends Animal)complex

Because typescript is a static type system and Animal is an unchanged class, it can be inferred that the content of angle brackets has been run before the class is created.

@type = (A extends Animal) => (new() => A): A

In other words, to use @type(T) to generate a type, first T must satisfy the Animal structure, and then you can get the required type. If T no longer meets the Animal class structure, the compiler will directly report an error. This error is not a type checking stage, but in the creation stage of the type system, that is, the running stage of the ts code. This situation is called "generic constraints".

In addition, syntaxes like <A,B> are actually consistent with function parameters.

@type = (A, B) => (A|B): SomeType

Let's look at the built-in basic type of ts: Array<number>

@Array = any => any[]

Conclusion

Generics in Typescript are actually parameters of type generation functions. The content of this article is all imagination out of thin air and is only applicable to the development of ideas when understanding ts, and is not applicable to real programming. This is hereby declared.

The above is the detailed content of how to explain TypeScript generics in a popular way. For more information about TypeScript generics, please follow my other related articles!