Generics are one of TypeScript’s more advanced features and can be difficult to understand. The generic usage scenario is so widespread that it can be seen in many places. When we read the source code for TS projects or use third-party libraries (like React) in our TS projects, we often see various generic definitions. If you don’t know much about generics, you probably won’t be able to use them, implement them, or even understand what they do.

I’m sure you’ve all experienced, seen, or written “an application where there’s a lot of duplicate type definitions, where the any type keeps popping up, and when you hover over a variable, it just says any, let alone type manipulation, it’s a problem to write it right.” I also went through this phase, when I was still relatively new to TS.

As I learn more about TS, I realize that “real TS masters are playing with types”, and make various operations on types to generate new types. This makes sense, after all, “What TS provides is a type system.” You look at the code of the TS masters and see all sorts of “fancy generics”. You can say that generics are a barrier, and until you get to grips with it, you know that “TS can play this way”. No wonder people ask about generics in interviews, even though the interviewer probably doesn’t know much about them either.

“Only by understanding the internal logic of things can we truly master them. Otherwise, we will never be able to master them.” This article takes you inside generics, gives you another look at what generics are, why they exist, and what difference they make to TS.

Note: Generics vary slightly from language to language, and knowledge transfer is possible, but not mechanically. All generics mentioned in this article refer to generics under TS.

The introduction

I concluded that there are two difficulties in learning TS. The first is a confusing notation for TS and JS, and the second is something unique to TS.

  • The confusing notation in TS

Such as:

Confusing arrow functions

(Easily confused arrow function)

Such as:


(easily confused parentheses in interface)

  • Something special in TS

Examples include Typeof, Keyof, infer, and generics in this article.

“Distinguish these from the confusing things in JS, and then understand the ts-specific things, especially generics” (the rest is basically relatively simple), and TS gets started.

First taste of generics

In strongly typed languages, you generally need to type a variable in order to use it. The following code:

const name: string = "lucifer";
console.log(name);
Copy the code

We need to declare name as a string before we can use the name variable, and we will get an error when we do the following.

  • Assign another type of value to name
  • Use methods specific to other type values (such as toFixed specific to Number)
  • Pass name as an argument to a function that does not support string. Such asdivide(1, name)Divide/divide/divide/divideDivide the first number (type number) by the second number (type number) and return the result.

TS provides some basic types (such as string above) that we can use directly. Also:

  • providesintefacetypeThe keyword allows us to define our own types, which we can then use just as we would with primitive types.
  • Provides a variety of logical operators, such as &, | etc, for us to type, so as to generate a new type.
  • Providing generics allows us to define a type in a general way without specifying a type, and specify specific parameter types when a function is called.
  • .

In other words, a generic type is a type, but unlike string, number, etc., it is an abstract type, and we cannot directly define a variable type to be generic.

In simple terms, generics program types instead of values. That sounds abstract. Then we’ll show you some examples of what this means. Just leave an impression.

To understand the above statement, first distinguish between “value” and “type”.

Values and types

Most of our code is “value programming”. Such as:

if (person.isVIP) {
    console.log('VIP')
}
if (cnt > 5) {
    // do something
}  const personNames = persons.map(p= > p.name) .Copy the code

As you can see, this is all about programming concrete values, “which fits our abstraction of the real world.” From the point of view of set theory, a set of values is a type, and the simplest use in TS is to qualify a type for a value, essentially a set of qualified values. The set can be a concrete set or a new set generated by a set operation (cross and union).


(Value and type)

Let’s look at a more specific example:

function t(name: string) {
  return `hello, ${name}`;
}
t("lucifer");
Copy the code

The string “lucifer” is a concrete “value” of string “type”. Here “lucifer” is the value, and string is the type.

[root@localhost] [root@localhost] [root@localhost] [root@localhost] [root@localhost] [root@localhost] [root@localhost]

t(123);
Copy the code

Because 123 is not an element in the string set.

For t(“lucifer”), the pseudo-code of TS judgment logic is:

v = getValue(); // will return 'lucifer' by ast
if (typeof v === "string") {
  // ok
} else {
  throw "type error";
} Copy the code

As a static type analysis tool, TS does not execute JS code, but that is not to say there is no execution logic inside TS.

To sum up simply: the set of values is a type, usually write code is basically value programming, TS provides a lot of “types” (can also be customized) and a lot of “type operations” to help us “limit values and operations on values”.

What are generics

So that’s kind of the setup, you know the difference between values and types, and what TS does for us. But it’s still a bit of a struggle to understand generics directly, so I’ll walk you through some examples.

Let’s start with a question: Why generics? In fact, there are many reasons for this, and HERE I choose a universally recognized entry point to explain. If you understand this point, other points are relatively easy to understand. Again, let’s take an example.

The ID function is not to be sniffed at

What would you do if you were to implement a function ID whose arguments can be any value, and whose return value is to return the arguments as they are, and which can take only one argument?

You’d think it would be easy to just write code like this:

const id = (arg) = > arg;
Copy the code

Some people might think that the ID function is useless. In fact, the ID function is very popular in functional programming.

Since it can take any value, that means your function’s input and return values should be of any type. Now let’s add a type declaration to our code:

type idBoolean = (arg: boolean) = > boolean;
type idNumber = (arg: number) = > number;
type idString = (arg: string) = > string;
.Copy the code

A clumsy approach like the one above means that as many types as JS provides, you need to copy the code and change the type signature. This is fatal to programmers. This kind of copy-and-paste increases the chance of errors, makes code difficult to maintain, and makes it all the more important. And when you add new types to JS in the future, you still need to modify the code, which means your code is “open to change”, which is not good. Another way is to use the “universal syntax” of any. What are the disadvantages? Let me give you an example:

id("string").length; // ok
id("string").toFixed(2); // ok
id(null).toString(); // ok
.Copy the code

If you use any, everything is OK and you lose the effect of type checking. In fact, I know that I sent you a string, and it must return a string, and there is no toFixed method on string, so I need to report an error. In other words, what I really want is that when I use an ID, you derive it from the type I passed to you. For example, if I pass in a string, but use a method on number, you should report an error.

To solve these problems, we “refactor the above code using generics.” In contrast to our definition, we use a type T, where “T is an abstract type whose value is determined only when it is called”, so we don’t have to copy and paste countless copies of code.

function id<T> (arg: T) :T {
  return arg;
}
Copy the code

Why is that ok? Why do I write it this way? What the hell is this Angle bracket? Everything has a cause and effect, and generics are designed this way for a reason. So let me explain to you, I’m sure many of you haven’t thought about this in this way.

Generics are about programming types

An important point to make is that while we normally program values, generics program types. I didn’t explain that to you. Now that’s enough foreshadowing, let’s get started!

To continue, let’s say we define a Person class that has three attributes, all of which are required. This Person class is used to qualify the form data when the user submits it.

enum Sex {
  Man,
  Woman,
  UnKnow,
}
interface Person {  name: string;  sex: Sex;  age: number; } Copy the code

All of a sudden, one day, the company wanted to carry out a promotional activity, which also needed the Person Shape, but these three attributes could be selected, and users were required to fill in their mobile phone number to mark the user and receive SMS messages. A clumsy way to do this is to write a new class:

interface MarketPerson {
name? :string;
sex? : Sex;age? :number;
  phone: string;
} Copy the code

Remember the repetition type definition I talked about at the beginning? This is!

This is clearly not elegant. What if the Person field is large? This duplication of code can be unusually heavy and not maintainable. The designers of TS certainly did not allow such an ugly design to exist. Can we generate new types from existing types? Of course you can! The answer is that I mentioned two types of operations on types: “One is set operations, and the other is generics, which I’ll cover today.”

Let’s start with the collection operation:

type MarketPerson = Person & { phone: string };
Copy the code

We added a mandatory field for phone, but did not specify name, sex, age. Imagine if we could “manipulate types like functions.” Would it be possible? For example, IF I define a Partial function that takes a type and returns a new type, all the properties in that type are optional.

Pseudo code:


function Partial(Type) {
Type ANS = Empty type    for(k in Type) {
Null Type [k] = makeOptional(Type, k) }  return ans }  type PartialedPerson = Partial(Person)  Copy the code

Unfortunately, the above code does not run, nor can it. Impossible to run for the following reasons:

  • You can see that I didn’t add the Partial signature. I did it on purpose. How would you add a signature to this function? Can’t add!
  • It is not appropriate to use JS syntax to manipulate types. First, this operation relies on the JS runtime, and TS is a static analysis tool and should not rely on the JS runtime. Secondly, in order to support this operation, whether it means that TS compromises JS, TS should support TS operation when JS produces new syntax (such as async await a few years ago).

There is an urgent need for a syntax that does not rely on JS behavior, especially runtime behavior, and whose logic is actually similar to the above, and does not conflict with existing syntactic systems. Let’s take a look at what the TS team did:

// Can be regarded as the above function definition, can accept any type. Since this is the "Type" parameter, it theoretically doesn't matter what name you call it, just like the parameter defined by the function.
type Partial<Type> = { do something }
// This can be thought of as a function call above, passing in the specific type Person
type PartialedPerson = Partial<Person>
Copy the code

Functionality aside, let’s see how similar the two are:


(definition)


(run)

Look again at the functionality of generics above. What this code means is that by processing T, it returns a subset of T, specifically making all properties of T optional. PartialedPerson = PartialedPerson

interface Person {
name? :string;
sex? : Sex;age? :number;
}
Copy the code

The function is the same as creating a new interface above, but more elegant.

Finally, look at the concrete implementation of generic Partial. It can be seen that it does not use JS syntax directly, but defines its own syntax, such as keyof here, which fully confirms my point above.

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

I just said, “Since it’s a parameter, it doesn’t matter what we call it.” So instead of Type, we have T, shorter. So this is sort of a convention, a specification, usually called T, U and so on for generic parameters.

Let’s see how full generics look like functions!


(definition)


(use)

  • Function becomes type and () becomes <>.

  • In terms of syntactic rules, the internal object of a function is ES standard. Generics correspond to a set of standards for TS implementation.



To put it simply, treating types as values and then programming them is the basic idea of generics. Generics are similar to the functions we normally use, except that they act on types and are not so different in idea from the functions we normally use. The concrete types produced by generics also support the operation of types. Such as:

type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
Copy the code

With that in mind, let’s reinforce it with a few examples.

function id<T.U> (arg1: T, arg2: U) :T {
  return arg1;
}
Copy the code

The generic ID is defined above, and its input arguments are T and U, separated by commas as function arguments. Once you have defined parameters, you can use them inside the function body. Above we use the parameters T and U in the parameter list and return value of the function.

The return value can also be a complex type:

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

(Generic parameters)

Similar to the above, except that the return value is an array.

It’s important to note that mentally we can understand it this way. However, there are some nuances to the implementation, such as:

type P = [number.string.boolean];
type Q = Date;

type R = [Q, ...P]; // A rest element type must be an array type.
Copy the code

Such as:

type Lucifer = LeetCode;
type LeetCode<T = {}> = {
  name: T;
};

const a: LeetCode<string>; //ok const a: Lucifer<string>; // Type 'Lucifer' is not generic. Copy the code

It is ok to change to this:

type Lucifer<T> = LeetCode<T>;
Copy the code

Why do generics use Angle brackets

Why do generics use Angle brackets (<>) and not something else? I guess because it looks the most like () and has no syntax ambiguity in current JS. However, it is not compatible with JSX! Such as:

function Form() {
  // ...
Copy the code

return ( <Select<string> options={targets} value={target} onChange={setTarget} /> ); }

That’s because WHEN TS invented the syntax, they didn’t even think of JSX. The TS team later fixed this issue in TypeScript 2.9. This means that you can now use JSX with generic parameters directly in TS (such as the code above).

The type of generic

In fact, in addition to function generics mentioned above, there are also interface generics and class generics. But the syntax and meaning are basically the same as function generics:

interface id<T, U> {
  id1: T;
  id2: U;
}
Copy the code

(Interface generics)

class MyComponent extends React.Component<Props.State> {
.}
Copy the code

(Generic type)

To summarize: the way to write generics is to add Angle brackets (<>) after the identifier, write parameters inside the Angle brackets, and do some logic with those parameters inside the body (function, interface, or class body).

Parameter types for generics – generic constraints

As we did at the beginning of this article, we can qualify the parameters of a function.

function t(name: string) {
  return `hello, ${name}`;
}
t("lucifer");
Copy the code

The above code type qualifies the function’s parameters so that the function accepts only string values. So how can generics achieve a similar effect?

type MyType = (T: constrain) = > { do something };
Copy the code

Again, in the case of the ID function, we add functionality to the ID function so that it can not only return parameters, but also print them out. For those of you familiar with functional programming, this is the trace function, which is used to debug programs.

function trace<T> (arg: T) :T {
  console.log(arg);
  return arg;
}
Copy the code

What if I wanted to print out the size property of the parameter? TS will report an error if the constraint is not applied at all:

Note: The message may differ from TS version to TS version. Mine is 3.9.5. All test results below are based on this version and will not be described herein.

function trace<T> (arg: T) :T {
  console.log(arg.size); // Error: Property 'size doesn't exist on type 'T'
  return arg;
}
Copy the code

The reason for the error is that T can theoretically be of any type, and unlike any, you will get an error no matter what property or method you use (unless the property or method is common to all collections). The intuitive idea is to restrict the “parameter type” passed to trace to size so that no errors are reported. How do you express this “type constraint” point? The key to implementing this requirement is the use of type constraints. You can do this using the extends keyword. Basically, you define a type and let T implement the interface.

interface Sizeable {
  size: number;
}
function trace<T extends Sizeable> (arg: T) :T {
  console.log(arg.size);
 return arg; }  Copy the code

In this case, T is no longer an arbitrary type, but the shape of the interface being implemented. Of course, you can inherit multiple interfaces. “Type constraints are a very common operation that you need to master.”

Some people might say can I just limit the Trace parameter to Sizeable? If you do this, you run the risk of type loss. See the article A Use Case for TypeScript Generics[1] for details.

Common generics

Collection classes

Array

, Array

, Array

This is actually a collection class and a generic type.


An array is essentially a collection of values that can be of any type. An array is just a container. However, in normal development, the item type of the array is usually the same, and there will be a lot of problems if it is not constrained. For example, if I’m supposed to be an array of strings but accidentally use the number method, the type system should help me identify the “type problem.”

Since array theory can hold any type, it requires the user to dynamically decide what type of data you want to store, and these types can only be determined when called. Array

is a call that produces a concrete collection that can only hold String values.


It is not allowed to call Array directly:

const a: Array = ["1"];
Copy the code

The above code is mistaken: Generic type ‘Array

‘ requires 1 type argument(s).ts. Does this sound like a function call failing to pass an argument? That’s right.

If you look at “Set” and “Promise”, you’ll soon know what it means. They are essentially wrapper types and support multiple parameter types, so they can be constrained by generics.

React.FC

If you’ve developed React TS applications, you know the React.FC type. Let’s see how it is defined [2] :

type FC<P = {}> = FunctionComponent<P>;

interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context? : any): ReactElement<any, any> |null;
propTypes? : WeakValidationMap<P>;contextTypes? : ValidationMap<any>;defaultProps? : Partial<P>;displayName? : string;} Copy the code

You can see the heavy use of generics. How can you understand generics if you don’t? No matter how complicated it is, let’s just start with a little bit, remember the analogy I gave you, and think of generics as functions. ,

  • First, we define a generic type FC, which is called React.FC. It is generated through another generic FunctionComponent.

So, in effect, the first line of code is just an alias

  • FunctionComponent is essentially an interface generic that defines five properties, four of which are optional and static class properties.
  • DisplayName is simpler, and propTypes, contextTypes, defaultProps are generated by other generics. We can still use this analysis method of mine to continue the analysis. For reasons of space, I will not analyze one by one here. Readers can try to analyze a wave by themselves after watching my analysis process.
  • (props: PropsWithChildren<P>, context? : any): ReactElement<any, any> | null;FunctionComponent is a function that takes two parameters (props and context) and returns either ReactElement or NULL. ReactElement should be familiar to you.PropsWithChildrenInsert children into props (props);
type PropsWithChildren<P> = P & { children? : ReactNode };Copy the code

Isn’t that the “set operations” and “optional properties” we talked about above? At this point, the full picture of React.FC is clear. Readers can test their learning by analyzing other source code, such as react. useState signatures.

Type derivation with default parameters

Type derivation and default parameters are two important TS functions that still apply to generics. Let’s take a look.

Type inference

Our common type derivation goes something like this:

const a = "lucifer"; // We do not declare a type for a, a is derived as string
a.toFixed(); // Property 'toFixed' does not exist on type 'string'.
a.includes("1"); // ok
Copy the code

It is important to note that type derivations are only inferred at initialization, and the following cannot be derived correctly:

let a = "lucifer"; // We do not declare a type for a, a is derived as string
a.toFixed(); // Property 'toFixed' does not exist on type 'string'.
a.includes("1"); // ok
a = 1;
a.toFixed(); // a cannot be derived as number
Copy the code

Generics also support type inference, as in the id function above:

function id<T> (arg: T) :T {
  return arg;
}
id<string> ("lucifer"); // This is ok, and the most complete way to write
id("lucifer"); // Based on type derivation, we can abbreviate it like this
Copy the code

That’s why there are two ways to write useState.

const [name, setName] = useState("lucifer");
const [name, setName] = useState<string> ("lucifer");
Copy the code

The actual type derivation is more complex and intelligent. It is believed that TS type derivation will become more intelligent as time goes on.

The default parameters

As with type derivation, default parameters can also reduce the amount of code, giving you less code. Premise is you want to understand, otherwise accompany you forever is big question mark. You can think of it as a function’s default argument.

Here’s an example:

type A<T = string> = Array<T>;
const aa: A = [1]; // type 'number' is not assignable to type 'string'.
const bb: A = ["1"]; // ok
const cc: A<number> = [1]; // ok
Copy the code

The above type A defaults to A string array. You can specify an Array without specifying it, or you can specify an Array type explicitly. One thing to note: in JS, functions are also a type of value, so:

const fn = (a)= > null; // ok
Copy the code

But generics don’t work that way, and that’s where functions differ (design flaw? Maybe) :

type A = Array; // error: Generic type 'Array<T>' requires 1 type argument(s).
Copy the code

The reason for this is the definition of Array:

interface Array<T> {
.}
Copy the code

If the Array type also supports default parameters, for example:

interface Array<T = string> {
.}
Copy the code

So type A = Array; If not specified, it defaults to string.

When to use generics

If you’ve read through this article, you should know when to use generics, so here’s a quick summary.

When your function, interface, or class:

  • When many types need to be applied, such as the generic declaration of the ID function we introduced.
  • You need to use Partial generics in a lot of places.

The advanced

As mentioned above, generics have a lot in common with normal functions. Ordinary functions can nest other functions, or even themselves, to form recursion. The same goes for generics!

Generics support function nesting

Such as:

type CutTail<Tuple extends any[]> = Reverse<CutHead<Reverse<Tuple>>>;
Copy the code

In the code above, Reverse reverses the argument list and CutHead cuts out the first item of the array. So what CutTail means is to reverse the list of parameters passed in, cutting the first parameter and reversing it back. In other words, cut the last item in the argument list. Function fn (a: string, b: number, c: Boolean): Boolean {} type cutTailFn = CutTail

String, b:number) => Boolean. For implementation, see Typescript’s complex generics practice: How do I cut the last parameter in a function argument list? [3]. Here, it’s enough to know that generics support nesting.

Generics support recursion

Generics can even nest themselves to form recursions, such as the most familiar definition of a single linked list.

type ListNode<T> = {
  data: T;
  next: ListNode<T> | null;
};
Copy the code

(Single linked list)

Or take the definition of “HTMLElement”.

declare var HTMLElement: {
    prototype: HTMLElement;
    new(): HTMLElement;
}; .Copy the code

(HTMLElement [4])

Above the “recursive declaration”, let’s look at a slightly more complex form of recursion – “recursive call”, which “recursively makes all properties of a type optional”. It’s similar to deep copy, except instead of copying, it becomes optional and applies to types, not values.

type DeepPartial<T> = T extends Function
  ? T
  : T extends object
  ? { [P inkeyof T]? : DeepPartial<T[P]> }  : T;
 type PartialedWindow = DeepPartial<Window>; // All properties on the window are now optional Copy the code

TS generic tools and implementation

Although generics support function nesting, and even recursion, but its syntactic ability is certainly not compared with JS, want to achieve a generic function is really not an easy thing. Here are a few examples, finish see this a few examples, believe you can reach at least level than gourd painting gourd ladle. So see more practice, slowly level up.

As of now (2020-06-21), TS offers 16 tool types [5].


(Official tool types)

In addition to the official tooltypes, there are also community tooltypes, such as Type-fest [6], which you can use directly or check out the source code to see how the pros play with types.

I’ll pick a few tool classes and give you an “implementation theory.”

Partial

The function is to “make optional” the attributes of a type. Notice that this is shallow Partial, DeepPartial as I showed you before, as long as it works with recursive calls.

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

Required

The Partial function is the opposite of Partial, which “makes mandatory” attributes of a type. -? It’s not optional. It’s mandatory.

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

Mutable

The function is to “make modifiable” the properties of a type, where – refers to removal. – Readonly means to remove the read – only meaning that it can be modified.

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

Readonly

Function is the opposite of a Mutable function, which “makes read-only” properties of a type. Adding readonly to a property makes it read-only.

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

ReturnType

The function is used to get the return value type of a function.

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

The following example uses ReturnType to get Func to return a value of type string, so foo can only be assigned as a string.

type Func = (value: number) = > string;

const foo: ReturnType<Func> = "1";
Copy the code

Ts-es5.d.ts [7] These generics can greatly reduce the amount of redundant code that you can customize in your own projects.

Bonus – Interface intelligence prompt

Finally, a practical tip. The following is the type definition of an interface:

interface Seal {
  name: string;
  url: string;
}
interface API {
 "/user": { name: string; age: number; phone: string };  "/seals": { seal: Seal[] }; } const api = <URL extends keyof API>(url: URL): Promise<API[URL]> => {  return fetch(url).then((res) = > res.json()); }; Copy the code

We implement intelligent hints through generics and generic constraints. Effect:


(Intelligent prompt for interface name)



(The interface returns an intelligent message)

The principle is very simple. When you input only API, it will prompt you for all keys under API interface. When you enter a key, it will match the type defined by interface according to the key, and then give the type prompt.

conclusion

Learning Typescript is not an easy task, especially if you don’t have a background in another language. One of the most difficult aspects of TS is probably generics.

Generics are very similar to the functions we use, and it is easy to understand if you compare the two. Many functions can be migrated to generics, such as function nesting, recursion, default parameters, and so on. Generics are programs for types where the parameter is a type and the return value is a new type. We can even constrain the parameters of generics, similar to the type constraints of functions.

Finally, a few advanced generic uses and a few generic utility classes are used to help you understand and digest the above knowledge. To know that the real TS masters are genre players, the masters will not be satisfied with genre crossover and manipulation. Using generics well can greatly reduce the amount of code and improve code maintainability. If you use it too deeply, you can cause team members to look at each other in bewilderment. Therefore, the level of abstraction must be reasonable, not just for generics, but for software engineering as a whole.

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.


Reference

[1]

A use case for TypeScript Generics: https://juliangaramendy.dev/when-ts-generics/


[2]

React.FC Type Definition: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts


[3]

Typescript complex generics practice: How do I cut the last parameter in a function argument list? : https://zhuanlan.zhihu.com/p/147248333


[4]

HTMLElement Type Definition: https://github.com/microsoft/TypeScript/blob/master/lib/lib.dom.d.ts


[5]

TS official 16 types of tools: https://www.typescriptlang.org/docs/handbook/utility-types.html#partialt


[6]

type-fest: https://github.com/sindresorhus/type-fest


[7]

TS – es5.d.ts: https://github.com/microsoft/TypeScript/blob/master/src/lib/es5.d.ts#L1431