Author: Dong Mo
Preface: When writing typescript applications, there are times when we want to reuse or construct types of a particular structure that are hard to express only from typescript with built-in types, interfaces, and classes. At this point we need to use type inference. To discuss type inference, we need to use generics and #infer. In this article we will only discuss generics
In the previous
The generic
Formally, generics in typescript are like generics in most languages (except for the as-yet-unimplemented Go :P) :
// one constrain for array-like object
interface ArrayLike<T> {
readonly length: number;
readonly [n: number]: T;
}Copy the code
ArrayLike above represents a class array structure, which represents objects that have the following characteristics:
- A read-only
length
Field; - The other fields must be numbers, and each field must correspond to a value of type T, where T is the generic flag
Array-like objects are ancient objects in Javascript, such as function (…) {… } function, whose arguments variables inside are typical array-like objects.
Generics are often used in these scenarios:
- Reserved words interface
- Reserved word type
- Class in the namespace (#namespace) (which is actually an interface at this point)
- Class at runtime
- Function definitions (including class constructors)
Let’s show in turn how type inference can be powerful in each of these scenarios.
Type derivation based on generics
When using generics for type inference, remember:
- Generics only serve static analysis of types, not Javascript runtime
- Logical extrapolation when thinking about type derivation should not be “what type will it get at run time “, but” what type will it get based on the properties of the type itself”
Point 2 May be a little hard to understand, but we’ll skip it and see what it means in the next passage.
Interface: T and keyof
Full key selectable
As mentioned in the previous layer, we can use keyof to extract all the key names of an interface. When generics are introduced, keyof can do even more interesting things:
/**
* Make all properties in T optional
*/
type Partial<T> = {
[P inkeyof T]? : T[P]; };Copy the code
Partial is for T(T needs to be of a type that can be treated as an interface), find all the key names, and rely on the key names to get a new interface that has the same structure but all the keys are optional.
For example, with forms, UninitForm can be an all-key optional version of it:
interface Form {
name: string
age: number
sex: 'male' | 'female' | 'other'
}
type UninitForm = Partial<Form>Copy the code
UninitForm is equivalent to:
{ name? : string; age? : number; sex? :"male" | "female" | "other";
}Copy the code
Full keys are required
In contrast, if you already have UninitForm, you can rely on Required to reach its all-key Required version:
/**
* Make all properties in T required
*/
type Required<T> = {
[P inkeyof T]-? : T[P]; };Copy the code
interface UninitForm { name? : string; age? : number; sex? :"male" | "female" | "other";
}
type AllKeyRequriedForm = Required<UninitForm>Copy the code
Full key read-only
With readonly, we can make all keys in an interface readonly
/**
* Make all properties in T readonly* /type Readonly<T> = {
readonly [P in keyof T]: T[P];
};Copy the code
Powerful extends ternary derivation
In the case of JS, extends is a reserved word for an extension class; In typescript, extends means type derivation when it appears in the following scenario:
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;Copy the code
Extends [DEP]? [RESULT1] : an expression for [RESULT2] is a typescript type derivation that follows the following rules:
If the generic T must satisfy the constraint of [DEP] (that is, T extends [DEP] is true), then the expression result is [RESULT1]. Conversely, the expression result is [RESULT2]:
- When [DEP] is a primitive type, if T is the corresponding primitive type, then
T extends [DEP]
为true
And vice versa forfalse
- When [DEP] is interface/class, if T must satisfy its constraints, then
T extends [DEP]
为true
And vice versa forfalse
- When [DEP] is void/never, the basic type is handled
- When [DEP] is a union type, the types that make up [DEP] are substituted into T, and the final result is the union type of the results of those operations
- If [DEP] is any
T extends [DEP]
Constant for thetrue
Following these rules, let’s analyze the Exclude.
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;Copy the code
Analysis shows that:
Exclude
There are two necessary generic tags inT
和U
(Because none of them provide default generics)- If T is a union type, then we get all types of T except U
So let’s apply Exclude
type NoOne = Exclude<1 | 2 | 3, 1> // NoOne = 2 | 3Copy the code
Also, to illustrate point 4 above, we can write a useless NotRealExclude;
type NotRealExclude<T, U> = T extends U ? U : T;
type Orig = NotRealExclude<1 | 2 | 3, 1> // Orig = 1 | 2 | 3Copy the code
Since NotRealExclude returns U if T does not conform to U and returns T if T conforms to U, the net result is that all the types that make up T are reassembled.
Extends a stew
Now that you know the basic use of extends, let’s look at some more examples:
Select, exclude, and recombine the keys of an object
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};Copy the code
Just as our usual pick function can extract a specific key-value pair from an object, pick can also extract a specific key-value definition from T.
interface Person { name: string age: number sex? :"male" | "female" | "other";
}
/**
* equivalent to { name: string }
*/
type SimplePersonInfo = Pick<Person, 'name'>Copy the code
If you can Pick, you can Omit anything
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;Copy the code
/**
* equivalent to { name: string, age: number }
*/
type SimplePersonInfo = Omit<Person, 'sex'>Copy the code
We know that in the index of an object, the in keyword can be used to extract the type from the union type as the key name of the interface, for example:
type Ks = 'a' | 'b' | 'c'
type KObject = {
[P in Ks]: any
}Copy the code
KObject can contain key names of ‘a’, ‘b’, and ‘c’.
With extends, we can easily build a dictionary of values of the same type from an interface, which is Record:
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};Copy the code
// construct one family with 3 keys: parent, mom, child
type Family = Record<'parent' | 'mom' | 'child', Person>Copy the code
This is equivalent to
interface Family {
parent: Person,
mom: Person,
child: Person
}Copy the code
Non-nullated variables, etc
/**
* Exclude null and undefined from T
*/
type NonNullable<T> = T extends null | undefined ? never : T;Copy the code
conclusion
At this point,
- We know how to use generics to make new types, right
- We know how to combine generics with the extends triplet and index in expression to disassemble and reassemble an interface
So far, however, we have only dealt with non-function interface(class)/type scenarios. Can we do something special for function interfaces, such as extracting the type of the second argument from an existing function definition? For the following func, can we extract the type of arg2?
interface func () {
(arg1: string, arg2: {
bar: string
}): void
}Copy the code
In the next article, we’ll discuss how to do this using the infer keyword.
other
Type inference based on generics is theoretically possible starting with typescript 2.8 (actually earlier, but typescript 2.8/3.5 is a landmark release, so it’s classified as such), starting with typescript 3.5, There are some official built-in types (#type) and interfaces (#interface) for derivation, and these are good examples for us to learn about type derivation. All examples used in this article come from typescript’s built-in lib.es5.d.ts.
Note that for generics, T is often used as the first choice for the generic tag; T is to generics what Foo is to sample code