Introduction to Generics
Generics can be understood as passing types as variables to type definitions, just like passing parameters to functions. For example:
By enclosing Type
in <>
, we can pass the type to the generic function. The effect of the above function is: it accepts an argument of type Type
and returns a result of type Type
.
Since Type
is a parameter, its name can be freely chosen. We often use T
as the parameter name, so it can be written like this:
Type Inference
When using a generic function, you can omit the explicit declaration of the type T
(and we usually do). In this case, TypeScript will automatically infer the type of T
:
In the example above, if the type <string>
is not explicitly specified, TypeScript will directly infer the type of "myString"
as T
, so the function will return a string.
Drawing a Circle
By default, generics can be any type, which reduces readability. When operating or calling methods on a type that has been “genericized”, it cannot pass the type check because it is of any type. To solve this problem, you can use extends
to draw a circle around the generic and restrict its type.
In the above code, <T extends Lengthwise>
indicates that T
must be a type with the length
property. Any type with the length
property satisfies the requirements of this generic, for example, it can also be an array.
Binding Ability
According to the official website, generics are used for type reuse, which is indeed very effective after the simple introduction above. But besides type reuse, what other applications does generics have?
My answer is the linkage of types. T can bind to other generics used within the same type definition.
Take a look at this example again, in fact, it binds the input type and the output type:
Now let’s look at a more obvious example of “type binding”.
Mapped Types
Suppose there is an object with keys ‘a’, ‘b’, and ‘c’, and the values are different functions. Now we need to get the type of an object with keys and corresponding function parameters. How can we achieve this?
Practice
Below is a question. If you are interested, you can think about how to improve the type of the following JS function. (The answer is below, don’t scroll down yet)
Question
First, we have a myMap
, but instead of using it directly, we wrap it with a wrapper
function. This allows us to perform some pre-processing before executing the function. Now, the question is, how should we define the type of wrappedMap
?
Answer
Now it is indeed achieved that the type of WrappedMap
is the return value of wrapper
, but the line (fn as any).apply(null, arg)
seems awkward, doesn’t it?
Why do we still need to set fn
as any
?
Because for TypeScript, a
, b
, c
are not bound to the parameter types of their values at all, so even if we use T
to restrict them, it has no effect. This sentence may be a bit convoluted, but the answer 2 below may be clearer.
Answer 2
The solution to remove (fn as any)
is to first create another map to map the things you need to associate, which is MyMapArgs
above, and then use this mapping to create MyMap
. This way, TypeScript finally understands that these two things are related.
P.S. For more detailed information, please refer to issues#30581 and pull#47109