Hello, everybody, this is Lilian. With the inexorable rise of Typescript, we believe that many of you have used Ts development at some point. In addition to the basic type definition, the use of Ts is relatively unfamiliar with Ts generics, built-in advanced types, and custom advanced type tools. This article dives into the Ts type tool principles and programming techniques through 22 type tool examples. No nonsense, the whole dry goods, the content is very much, want to improve the SKILLS of Ts partners please read patiently. I believe that after reading this article, you can have a deeper understanding of this. Below, let’s begin ~
This paper is divided into three parts:
- The first part covers some of the basic keyword features (such as index query, index access, mapping, extends, etc.), but this section covers more of the features that aren’t clear, and leaves the basic features alone. More key words and techniques will be included in the following examples to demonstrate the details;
- The second part introduces Ts built-in type tools and their implementation principles, such as Pick, Omit, etc. Part three covers custom types;
- The third part, which is the most difficult part, will walk you through some of the more complex type tool examples and explain the obscure areas and the knowledge involved. This section will also contain a lot of Ts type tools programming skills, also hope that through this part of the explanation, the friends of Ts foundation can be further improved!
The first part of the pre-content
keyof
Indexing query
Corresponding to any type T, the result of keyof T is the union of all common attribute keys on that type:
interface Eg1 {
name: string.readonly age: number,}/ / the type of T1 is the name | age
type T1 = keyof Eg1
class Eg2 {
private name: string;
public readonly age: number;
protected home: string;
}
// T2 is actually constrained to age
Name and home are not public properties, so they cannot be obtained by keyof
type T2 = keyof Eg2
Copy the code
T[K]
Index access
interface Eg1 {
name: string.readonly age: number,}// string
type V1 = Eg1['name']
// string | number
type V2 = Eg1['name' | 'age']
// any
type V2 = Eg1['name' | 'age2222']
// string | number
type V3 = Eg1[keyof Eg1]
Copy the code
T[keyof T] is used to obtain the union type of all key types of T. T[keyof K], to get the key in T and at the same time in the K type of the union type; Note: if the key in [] does not exist in T, then any; Because ts also doesn’t know what type the key ends up being, it’s any; And will also report an error;
&
Cross type note
A union of types taken by a cross type, but if the same key is of a different type, then the key is never.
interface Eg1 {
name: string.age: number,}interface Eg2 {
color: string.age: string,}/** * T is of type {name: string; age: number; Age: never} * Note that age has different types in Eg1 and Eg2, so the type of age after crossing is never */
type T = Eg1 & Eg2
// You can verify this by using the following example
const val: T = {
name: ' '.color: ' '.age: (function a() {
throw Error}}) () (),Copy the code
Extends keyword feature (emphasis)
- Used for an interface to indicate inheritance
interface T1 {
name: string,}interface T2 {
sex: number,}/ * * *@example
* T3 = {name: string, sex: number, age: number}
*/
interface T3 extends T1, T2 {
age: number,}Copy the code
Note that the interface supports multiple inheritance, separated by commas. If type implements inheritance, the crossover type type A = b&c&d can be used.
- Indicates the condition type, which can be used for condition judgment
Returns the first parameter after the question mark if the previous condition is met, and the second parameter otherwise. Similar to js ternary operations.
/ * * *@example
* type A1 = 1
*/
type A1 = 'x' extends 'x' ? 1 : 2;
/ * * *@example
* type A2 = 2
*/
type A2 = 'x' | 'y' extends 'x' ? 1 : 2;
/ * * *@example
* type A3 = 1 | 2
*/
type P<T> = T extends 'x' ? 1 : 2;
type A3 = P<'x' | 'y'>
Copy the code
Question: Why are A2 and A3 different values?
- If used for simple conditional determination, it is directly to determine whether the preceding type is assignable to the following type
- if
extends
The preceding type is generic, and when a generic type is passed in as a union type, it is determined in turn whether all subtypes of that union type can be assigned to the type following extends (a distribution process).
So in conclusion,extends
When the preceding parameter is a union type, the union type is decomposed (iterating through all subtypes in order to determine the condition). The final results are then grouped into new union types.
- Prevents the extends keyword from distributing properties to union types
If you don’t want to be split (distributed), it’s also easy to wrap the following with a simple tuple type:
type P<T> = [T] extends ['x']?1 : 2;
/** * type A4 = 2; * /
type A4 = P<'x' | 'y'>
Copy the code
Distributed feature documents for conditional types
Type compatibility
In set theory, A is A subset of B if all the elements of A set exist in B;
In the type system, a type is a subtype if its attributes are more specific. (Because there are fewer attributes, it means that the type is more restricted and is the parent type.)
Therefore, we can draw the basic conclusion: subtypes are more specific than parent types, and parent types are more general than subtypes. In the following, we will also further explain the type-based replicability (distributability), covariant, contravariant, bidirectional covariant, etc.
- Sex can be assigned a value
interface Animal {
name: string;
}
interface Dog extends Animal {
break() :void;
}
let a: Animal;
let b: Dog;
Subtypes are more specific. Subtypes can be assigned to more general parent types
a = b;
// Not the other way around
b = a;
Copy the code
- The property of assignability in union types
type A = 1 | 2 | 3;
type B = 2 | 3;
let a: A;
let b: B;
// No assignment
b = a;
// Can be assigned
a = b;
Copy the code
Does A have more types and A is A subtype? On the contrary, A has more types but expresses A wider range of types, so A is the parent type and B is the subtype.
Thus b = a does not hold (a parent type cannot be assigned to a subtype), but a = b does (a subtype can be assigned to a parent).
- covariance
interface Animal {
name: string;
}
interface Dog extends Animal {
break() :void;
}
let Eg1: Animal;
let Eg2: Dog;
// Compatible, can be assigned
Eg1 = Eg2;
let Eg3: Array<Animal>
let Eg4: Array<Dog>
// Compatible, can be assigned
Eg3 = Eg4
Copy the code
Array
can still be assigned to Array
after Animal and Dog become arrays, so it is covariant for type MakeArray = Array
.
To quote the definition from Wikipedia:
Covariance and contravariance is a term used in computer science to describe whether there is a parent/child relationship among multiple complex types that are constructed by means of type constructors.
To put it simply, if multiple types have a parent relationship, a new type is constructed through some kind of construction relationship, if the parent relationship is also covariant, and if the relationship is reversed (children change fathers, fathers change children), it is contravariant. This may sound abstract, but let’s use a more concrete example to illustrate:
- inverter
interface Animal {
name: string;
}
interface Dog extends Animal {
break() :void;
}
type AnimalFn = (arg: Animal) = > void
type DogFn = (arg: Dog) = > void
let Eg1: AnimalFn;
let Eg2: DogFn;
// No longer can be assigned,
AnimalFn = DogFn; Animal = Dog
Eg1 = Eg2;
// The reverse is true
Eg2 = Eg1;
Copy the code
If Animal = Dog is type-safe, then AnimalFn = DogFn should also be type-safe. Why does Ts think Animal = DogFn is unsafe? Look at the following example:
let animal: AnimalFn = (arg: Animal) = > {}
let dog: DogFn = (arg: Dog) = > {
arg.break();
}
// Assume type-safe assignment
animal = dog;
// When animal is called, the parameter of the constraint is missing, which causes an error
animal({name: 'cat'});
Copy the code
As you can see from this example, if the dog function is assigned to the animal function, then the animal function is called with the constraint that the parameter must be animal (not dog), but the animal is actually dog, and then an error will occur.
So Animal and Dog are “inverting” after the type Fn
= (arg: T) => void constructor.
- Two-way covariance
Ts actually defaults to a bidirectional covariant strategy in the comparison of function arguments: the assignment succeeds only if the source function arguments can be assigned to the target function or vice versa.
This is unstable, because the caller might pass in a function with more precise type information, but call the passed function with less precise type information (typically the inverter above). In practice, however, this is very error-prone and can implement many of the common patterns found in JavaScript:
// Interface definition of EventListener in lib.dom.d.ts
interface EventListener {
(evt: Event): void;
}
// The simplified Event
interface Event {
readonly target: EventTarget | null;
preventDefault(): void;
}
// Simplify the merged MouseEvent
interface MouseEvent extends Event {
readonly x: number;
readonly y: number;
}
// Simplified Window interface
interface Window {
// Simplified addEventListener
addEventListener(type: string.listener: EventListener)
}
// For daily use
window.addEventListener('click'.(e: Event) = > {});
window.addEventListener('mouseover'.(e: MouseEvent) = > {});
Copy the code
As you can see, the Listener function of Window requires that the parameter be Event, but more often than not, the subtype Event is passed in. But it works here, which is why its default behavior is bidirectional covariant. You can modify the strictFunctionType attribute in tsconfig.js to strictly control covariant and inverter.
Hit the key!! Hit the key!! Hit the key!!
The function of infer keyword will not be explained in detail for now. It is mainly used to allow Ts to push itself to the type in the condition type of extends. For details, please refer to the official website. But infer has some important properties that are easy to be ignored:
infer
If the derivation has the same name and is in the contravariant position, the result of the derivation will be of crossover type.
type Bar<T> = T extends {
a: (x: infer U) = > void;
b: (x: infer U) = > void; }? U :never;
// type T1 = string
type T1 = Bar<{ a: (x: string) = > void; b: (x: string) = > void} >.// type T2 = never
type T2 = Bar<{ a: (x: string) = > void; b: (x: number) = > void} >.Copy the code
infer
If the derivation has the same name and is in a covariant position, the result of the derivation will be of crossover type.
type Foo<T> = T extends {
a: infer U; b: infer U; }? U :never;
// type T1 = string
type T1 = Foo<{ a: string; b: string} >.// type T2 = string | number
type T2 = Foo<{ a: string; b: number} >.Copy the code
The second part of Ts built-in type tool principle analysis
Partial implementation principle analysis
Partial
makes all properties of T optional.
/** * The core implementation is to iterate over all properties on T by mapping type, * and then set each property as optional */
type Partial<T> = {
[P inkeyof T]? : T[P]; }Copy the code
[P in keyof T]
Traversal by mapping typeT
All properties on? :
Setting the property to be optionalT[P]
Set the type to the original type
By extension, make the specified key optional:
* keyof T is the union type of all keys of T */
type PartialOptional<T, K extends keyof T> = {
[P inK]? : T[P]; }/ * * *@example* type Eg1 = { key1? : string; key2? : number } */
type Eg1 = PartialOptional<{
key1: string.key2: number.key3: ' '
}, 'key1' | 'key2'>;
Copy the code
Readonly Principle analysis
/** * The main implementation is to map through all keys, * and add a readonly modifier */ to each key
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
/ * * *@example
* type Eg = {
* readonly key1: string;
* readonly key2: number;
* }
*/
type Eg = Readonly<{
key1: string.key2: number,
}>
Copy the code
Pick
Pick a set of attributes and form a new type.
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
Copy the code
Basically the same knowledge and above, no longer repeat.
Record
Construct a type with key for each subtype of the union type and type T. The text is difficult to understand, let’s look at an example:
/ * * *@example* type Eg1 = { * a: { key1: string; }; * b: { key1: string; }; * *}@desc Is to traverse the first parameter to the 'a' | 'b' each subtype, and then set the value as the second parameter * /
type Eg1 = Record<'a' | 'b', {key1: string} >Copy the code
Specific implementation of Record:
/** * The core implementation is to iterate over K and set the value to T */
type Record<K extends keyof any, T> = {
[P in K]: T
}
/ * * *@example
* type Eg2 = {a: B, b: B}
*/
interface A {
a: string.b: number,}interface B {
key1: number.key2: string,}type Eg2 = Record<keyof A, B>
Copy the code
- It’s worth noting
keyof any
Get isstring | number | symbol
- The reason is that type key can only be of type
string | number | symbol
Extensions: homomorphism and non-homomorphism. Underline!! Underline!! Underline!!
Partial
,Readonly
andPick
Are homomorphic, that is, their implementations require input type T to copy the property, so property modifiers (such as readonly,? 🙂 will be copied. This can be verified by the following example:
/ * * *@example* type Eg = {readonly a? : string} */
type Eg = Pick<{readonlya? :string}, 'a'>
Copy the code
As you can see from the result of Eg, Pick copies readonly and? Modifiers for:.
Record
It is non-homomorphic and does not need to copy the property, so it does not copy the property modifier
Why does “Pick” copy the property, but “Record” does not? Let’s compare the implementation:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Record<K extends keyof any, T> = {
[P in K]: T
}
Copy the code
As you can see in the implementation of Pick, notice that P in K (essentially P in keyof T), T is the input type, and keyof T traverses the input type; In the implementation of Record, all input types are not traversed. K is just a subtype of keyof any.
Finally, the Pick, Partial, and readonly tools all use keyof T to help copy the properties of the incoming type.
Analysis of Exclude principle
Exclude
extracts the union type composed of types that exist in T but not in U.
/** * Iterates over all subtypes in T, returning never if the subtype is restricted to U (exists in U and is compatible with U), or */ otherwise
type Exclude<T, U> = T extends U ? never : T;
/ * * *@example
* type Eg = 'key1'
*/
type Eg = Exclude<'key1' | 'key2'.'key2'>
Copy the code
Hit the key!!
never
Represents a type that does not existnever
After association with other types, there is nonever
the
/**
* @example
* type Eg2 = string | number
*/
type Eg2 = string | number | never
Copy the code
So the above Eg actually equals key1 | never, namely type Eg = key1
Extract
Extract
Extract all intersections of union type T and union type U.
type Extract<T, U> = T extends U ? T : never;
/ * * *@example
* type Eg = 'key1'
*/
type Eg = Extract<'key1' | 'key2'.'key1'>
Copy the code
Omit principle analysis
Omit all attributes of Omit
from type T.
** * Use Pick to achieve Omit */
type Omit = Pick<T, Exclude<keyof T, K>>;
Copy the code
- Another way to think about it, the implementation could be using
Pick
Extract the types of keys that we need - So it’s going to be
Omit = Pick<T, we need attributes union >
- And the attribute union that we need is, from the attribute union of T to the one that exists in the union type K
- So it’s going to be
Exclude<keyof T, K>
;
What if you don’t use Pick?
/** * Use mapping type Omit */
type Omit2<T, K extends keyof any> = {
[P in Exclude<keyof T, K>]: T[P]
}
Copy the code
- The implementation is similar to the principle implementation of Pick
- The difference is that we need different properties for traversal
- The property we need is the same as in the example above, which is
Exclude<keyof T, K>
- So traversal is going to be
[P in Exclude<keyof T, K>]
The Parameters and ReturnType
Parameters gets the parameter types of the function, putting each parameter type in a tuple.
/ * * *@desc Specific implementation */
type Parameters<T extends(... args:any) = >any> = T extends(... args: infer P) =>any ? P : never;
/ * * *@example* type Eg = [arg1: string, arg2: number]; * /
type Eg = Parameters<(arg1: string, arg2: number) = > void>;
Copy the code
Parameters
First constraint parametersT
It has to be a function, so(... args: any) => any>
replaceFunction
It’s ok- Implementation is judgment
T
Is it a function type? If it is, use itinter P
Let TS derive the parameter types of the functions and store the results in the typesP
Up, or backnever
;
Hit the key!! Hit the key!! Hit the key!!
inter
The purpose of the keyword is to let Ts derive the type itself and store the result of the derivation on the type bound to its parameter. Eg:inter P
The result is stored on type P for use.inter
Keywords can only be used on the extends condition type and not anywhere else.
Knock the key again!! Knock the key again!! Knock the key again!!
-
Type Eg = [arg1: string, arg2: number] This is a tuple, but it is the same as the common tuple type tuple = [string, number]. This part of the documentation is not mentioned on the official website, but you can think of it as something similar to a named tuple, or named tuple. There’s essentially nothing special about it, like it can’t be evaluated by this name. But from the semantic point of view, personally feel more semantic expression.
-
The optional options that define the ancestor can only be the last option
/** * Common way */
type Tuple1 = [string.number? ] ;const a: Tuple1 = ['aa'.11];
const a2: Tuple1 = ['aa'];
/** * Name */
type Tuple2 = [name: string, age? :number];
const b: Tuple2 = ['aa'.11];
const b2: Tuple2 = ['aa'];
Copy the code
The infer extension implements a infer array of all the elements of the type:
If T is of an array type, then derive the types of the elements in the array
type FalttenArray<T extends Array<any>> = T extends Array<infer P> ? P : never;
/** * type Eg1 = number | string; * /
type Eg1 = FalttenArray<[number.string] >/** * type Eg2 = 1 | 'asd'; * /
type Eg2 = FalttenArray<[1.'asd'] >Copy the code
ReturnType Gets the return value type of the function.
/ * * *@desc The implementation of ReturnType is basically the same as that of Parameters * except that infer R is used in different positions. * /
type ReturnType<T extends(... args:any) = >any> = T extends(... args:any) => infer R ? R : any;
Copy the code
ConstructorParameters
ConstructorParameters takes the parameter types of a class’s constructor, which exist in a tuple.
/** * The core implementation is to use infer to derive the constructor parameter types */
type ConstructorParameters<T extends abstract new(... args:any) = >any> = T extends abstract new(... args: infer P) =>any ? P : never;
/ * * *@example* type Eg = string; * /
interface ErrorConstructor {
new(message? :string) :Error; (message? :string) :Error;
readonly prototype: Error;
}
type Eg = ConstructorParameters<ErrorConstructor>;
/ * * *@example* type Eg2 = [name: string, sex?: number]; * /
class People {
constructor(public name: string, sex? :number){}}type Eg2 = ConstructorParameters<typeof People>
Copy the code
- First constraint parameters
T
Is the class that has the constructor. Notice there’s aabstract
Modifier, which I’ll show you in a second. - When implemented, judge
T
Is a class that satisfies the constraintinfer P
Automatically deduces the type of the argument to the constructor and returns that type.
Hit the key!! Hit the key!! Hit the key!!
So why do we constrain T to be abstract? Here’s an example:
/** * define a generic class */
class MyClass {}
/** * define an abstract class */
abstract class MyAbstractClass {}
// Can be assigned
const c1: typeof MyClass = MyClass
// Failed to assign an abstract constructor type to a non-abstract constructor type
const c2: typeof MyClass = MyAbstractClass
// Can be assigned
const c3: typeof MyAbstractClass = MyClass
// Can be assigned
const c4: typeof MyAbstractClass = MyAbstractClass
Copy the code
Thus, if the type is defined as an abstract class (abstract constructor), it can be assigned to either an abstract class or a normal class. And not the other way around.
Knock the key again!! Knock the key again!! Knock the key again!!
What’s the difference between using a class as a type and using a Typeof class as a type?
/** * define a class */
class People {
name: number;
age: number;
constructor(){}}// p1 can be assigned normally
const p1: People = new People();
Typeof People is missing the following attributes in type "People" : name, age
const p2: People = People;
// p3 error: "prototype" is missing in type" People", but is required in type" typeof People"
const p3: typeof People = new People();
// p4 can be assigned normally
const p4: typeof People = People;
Copy the code
The conclusion was this:
- When a class is directly a type, the type constraint is that the type must be an instance of the class; That is, the type gets instance properties and instance methods (also called stereotype methods) on the class.
- When a typeof class is used as a type, the typeof the constraint satisfies that class; That is, the type gets static properties and methods on the class.
Finally, we can get the type of the value returned by the constructor simply by repositioning the use of inter:
type InstanceType<T extends abstract new(... args:any) = >any> = T extends abstract new(... args:any) => infer R ? R : any;
Copy the code
Ts Compiler is an internal implementation type
- Uppercase
/ * * *@desc Construct a type * to uppercase a string@example* type Eg1 = 'ABCD'; * /
type Eg1 = Uppercase<'abcd'>;
Copy the code
- Lowercase
/ * * *@desc Construct a type * that converts a string to small uppercase@example* type Eg2 = 'abcd'; * /
type Eg2 = Lowercase<'ABCD'>;
Copy the code
- Capitalize
/ * * *@desc Construct a type * that capitalizes the first character of a string@example* type Eg3 = 'abcd'; * /
type Eg3 = Capitalize<'Abcd'>;
Copy the code
- Uncapitalize
/ * * *@desc Construct a type * that converts the first character of a string to lowercase@example* type Eg3 = 'ABCD'; * /
type Eg3 = Uncapitalize<'aBCD'>;
Copy the code
These types of tools are not defined in the lib.es5.d.ts file:
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;
Copy the code
Part three: Custom Ts advanced type tools and type programming techniques
SymmetricDifference
SymmetricDifference
Gets a type that does not exist in both T and U.
/** * Core implementation */
type SymmetricDifference<A, B> = SetDifference<A | B, A & B>;
/** * SetDifference implements the same as Exclude */
type SymmetricDifference<T, U> = Exclude<T | U, T & U>;
/ * * *@example* type Eg = '1' | '4'; * /
type Eg = SymmetricDifference<'1' | '2' | '3'.'2' | '3' | '4'>
Copy the code
Its core implementation makes use of three points: distributed union types, crossover types, and Exclude.
- We first use Exclude to retrieve the type that exists in the first argument but not in the second
Exclude
The second parameter isT & U
What you get is a cross type for all typesExclude
The first argument isT | U
This takes advantage of the distribution feature of the union type in extends, which can be understood asExclude<T, T & U> | Exclude<U, T & U>
;
So in summary, you take the types that exist in T but don’t exist in T & U, and then you take the types that exist in U but don’t exist in T & U, and you combine them.
FunctionKeys
Gets the union type of all keys in T whose type is a function.
/ * * *@desc NonUndefined Determines if T is undefined */
type NonUndefined<T> = T extends undefined ? never : T;
/ * * *@desc Core implementation */
type FunctionKeys<T extends object> = {
[K in keyof T]: NonUndefined<T[K]> extends Function ? K : never;
}[keyof T];
/ * * *@example* type Eg = 'key2' | 'key3'; * /
type AType = {
key1: string.key2: () = > void.key3: Function};type Eg = FunctionKeys<AType>;
Copy the code
- First, the constraint parameter T is of type
object
- By mapping type
K in keyof T
Iterate over all keys and pass firstNonUndefined<T[K]>
filterT[K]
forundefined | null
If not, return never - if
T[K]
Is a valid typeFunction
Type, return if yesK
, otherwise,never
; The type can be obtained at this point, for example:
/** * the above Eg should be of the following type, pseudocode: */
type TempType = {
key1: never.key2: 'key2'.key3: 'key3',}Copy the code
- Finally, after
{omit}[keyof T]
Index access to the union type of the value typenever | key2 | key3
, is equal tokey2 | key3
;
Hit the key!! Hit the key!! Hit the key!!
T[]
Is the type of index access operation that can fetch the valueT['a' | 'b']
if[]
If the internal parameter is a federated type, it is also a feature of the distribution index, and the types of values are federated in turnT[keyof T]
Is access toT
Type type of all values;never
And other types,never
It doesn’t exist. Such as:never | number | string
Is equivalent tonumber | string
Knock the key again!! Knock the key again!! Knock the key again!!
null
andundefined
You can assign to other types (except for strict assignment tests to start that type), so it is needed in the above implementationNonUndefined
Antecedent judgment.NonUndefined
In the implementation, only to judgeT extends undefined
In fact, the two can be compatible with each other. So you switch toT extends null
orT extends null | undefined
It’s all ok.
// A = 1
type A = undefined extends null ? 1 : 2;
// B = 1
type B = null extends undefined ? 1 : 2;
Copy the code
Finally, if you want to write a union type that gets a key that is not a function type, you simply have a different position for K and never. Likewise, you can implement StringKeys, NumberKeys, and so on. But remember to abstract a factory type:
type Primitive =
| string
| number
| bigint
| boolean
| symbol
| null
| undefined;
/ * * *@desc Used to create a type factory * that gets the specified type tool@param T The type to be extracted *@param P The type to create *@param IsCheckNon Whether to perform null and undefined checks */
type KeysFactory<T, P extends Primitive | Function | object, IsCheckNon extends boolean> = {
[K in keyof T]: IsCheckNon extends true
? (NonUndefined<T[K]> extends P ? K : never)
: (T[K] extends P ? K : never);
}[keyof T];
/ * * *@exampleFor example, KeysFactory can be created from the factory type
type FunctionKeys<T> = KeysFactory<T, Function.true>;
type StringKeys<T> = KeysFactory<T, string.true>;
type NumberKeys<T> = KeysFactory<T, string.true>;
Copy the code
MutableKeys
MutableKeys
finds the union type of all optional keys of T.
/** * Core implementation */
type MutableKeys<T extends object> = {
[P inkeyof T]-? : IfEquals< { [Qin P]: T[P] },
{ -readonly [Q in P]: T[P] },
P
>;
}[keyof T];
/ * * *@desc An auxiliary type that determines whether X and Y are of the same type, *@returns If yes, return A, otherwise return B */
type IfEquals<X, Y, A = X, B = never> = (<T>() = > T extends X ? 1 : 2) extends (<T>() = > T extends Y ? 1 : 2)? A : B;Copy the code
To implement MutableKeys, we need to divide it into the following steps:
The first step is to understand some of the features of read-only and non-read-only
/** * iterates over type T and returns it intact
type RType1<T> = {
[P in keyof T]: T[P];
}
/** * iterates over type T, making each key non-read-only *. * /
type RType2<T> = {
-readonly[P in keyof T]: T[P];
}
// R0 = { a: string; readonly b: number }
type R0 = RType1<{a: string.readonly b: number} >// R1 = { a: string }
type R1 = RType1<{a: string} >.// R2 = { a: string }
type R2 = RType2<{a: string} >.// R3 = { readonly a: string }
type R3 = RType1<{readonly a: string} >.// R4 = { a: string }
type R4 = RType2<{readonly a: string} >.Copy the code
When RType1 and RType2 are non-read-only attributes, the results for R1 and R2 are the same. When RType1 and RType2 are read-only properties, the result is that R3 is read-only and R4 is non-read-only. So, here’s the key:
[P in Keyof T]
Is a mapping type, and a mapping is a homomorphism, which copies the original attribute modifier, etc. Look at the example for R0.- Mapping type
-readonly
Represented as aNot read-onlyOr can be understood as removeread-only. forread-onlyProperties and-readonly
Turned out to beNot read-only, and forNot read-onlyProperties and-readonly
afterNot read-only. A common way to use this is if you want to make a property non-read-only, you can’t leave it unqualifier (although not writing means non-read-only), but consider homomorphic copying.
Second, parse the IfEquals
IfEquals is used to determine whether types X and Y are the same. If they are, return A, otherwise return B. This function is relatively difficult, do not be afraid, the next talk is no problem to understand ~
type IfEquals<X, Y, A = X, B = never> =
(<T>() = > T extends X ? 1 : 2) extends
(<T>() = > T extends Y ? 1 : 2)? A : B;Copy the code
- First of all,
IfEquals<X, Y, A, B>
Four parameters of,X and Y
Is the two types to be compared, and returns if they are equalA
Is not equally returnedB
. IfEquals
The basic skeleton of PI isType IfEquals<> = (parameter 1) extends (parameter 2)? A : B
If the type of argument 1 can be assigned to the type of argument 2, then returnA
Otherwise returnB
;- The basic structure of argument 1 and argument 2 is the same, the only difference is that X and Y are different. Here’s an example:
// A =
() => T extends string ? 1:2;
type A = <T>() = > T extends string ? 1 : 2;
// B =
() => T extends number ? 1:2;
type B = <T>() = > T extends number ? 1 : 2;
// C = 2
type C = A extends B ? 1 : 2;
Copy the code
Isn’t it strange that it follows that type A and type B are different? Here’s the answer:
- This takes advantage of a feature of the Ts compiler, which assumes that if two types (such as here
X
andY
) is used only to constrain two identical generic functions. This may seem strange to understand, or logically incorrect (because counterexamples can be given), but the Ts development team has guaranteed that this feature will not change in the future. May refer tohere. - Note that attribute modifiers are also judged here, for example
readonly
.Optional attribute
Wait, see through the following example to verify:
/** * T2 has more readonly modifiers than T1 * T3 has more optional modifiers than T1 * this controls a single variable for validation */
type T1 = {key1: string};
type T2 = {readonly key1: string};
typeT3 = {key1? :string};
// A1 = false
type A1 = IfEquals<T1, T2, true , false>;
// A2 = false
type A2 = IfEquals<T1, T3, true , false>;
Copy the code
IfEquals
Finally, there is the use of 1 and 2 to assist judgment (grammatical level), and there is to giveA
The default value of isX
.B
The default value of isnever
.
Finally, if you’re A lovey-dovey little baby, you might have A soul-searching question for me: Why not just use type IfEquals
= X extends Y to determine whether types are equal? A: And B? Both simple and rude (PS: from your evil charm smile ~). For the answer, let’s look at the following example:
type IfEquals<X, Y, A, B> = X extends Y ? A : B;
/** * use the example above */
type T1 = {key1: string};
type T2 = {readonly key1: string};
typeT3 = {key1? :string};
// A1 = true
type A1 = IfEquals<T1, T2, true , false>;
// A2 = true
type A2 = IfEquals<T1, T3, true , false>;
Copy the code
The obvious answer is that you really can’t do anything with modifiers like readonly. Kua claw Kill ~ ~ ~
Step 3: ParseMutableKeys
Implementation logic
MutableKeys
First constrains T to be of type object- By mapping type
[P in keyof T]
To iterate, the value of key isIfEquals< Type 1, Type 2, P>
Return P (key) if type 1 and type 2 are equal, otherwise return never.
P is a union type with only one current key, so [Q in P]: T[P] is just a normal mapping type. Note, however, that argument 1{[Q in P]: T[P]} is a type constructed from {}, and argument 2{-readonly [Q in P]: T[P]} is a type constructed from {}. The only difference between the two is -readonly.
So it’s interesting to think back to the first step example above: if P is read only, then P for arguments 1 and 2 will eventually be read only; If P is non-read-only, then P of argument 1 is non-read-only, and P of argument 2 becomes read-only after the non-read-only attribute is removed by -readonly. So the filtering is complete: P is the P returned by IfEquals when it is not read-only, and IfEquals returns never when P is read-only.
- So when the key is non-read-only, the type is
key
Otherwise, the type isnever
And finally passed[keyof T]
Gets the union type for all non-read-only keys.
OptionalKeys
OptionalKeys
Extracts the union type of keys of all optional types in T.
type OptionalKeys<T> = {
[P in keyof T]: {} extends Pick<T, P> ? P : never
}[keyof T];
typeEg = OptionalKeys<{key1? :string.key2: number} >Copy the code
- Core implementation, using the mapping type to traverse all keys, through
Pick<T, P>
Extract the current key and type. Note that this also takes advantage of the fact that homomorphic copies copy optional modifiers. - using
{} extends {current key: type}
Check whether the type is optional.
// Eg2 = false
type Eg2 = {} extends {key1: string}?true : false;
// Eg3 = true
type Eg3 = {} extends{key1? :string}?true : false;
Copy the code
Using {} and containing only the optional argument type {key? : string} is compatible with this feature. It’s also possible to replace the {} before extends with object.
Strengthen the Pick
- PickByValue Retrieves the type of the specified value
// The helper function is used to get the union type of keys in T whose type cannot be never
type TypeKeys<T> = T[keyof T];
/** * Core implementation */
type PickByValue<T, V> = Pick<T,
TypeKeys<{[P in keyof T]: T[P] extends V ? P : never} > >;/ * * *@example
* type Eg = {
* key1: number;
* key3: number;
* }
*/
type Eg = PickByValue<{key1: number.key2: string.key3: number}, number>;
Copy the code
Ts type compatibility features, so similar string can be assigned to a string | number, so the above is not precise way of extraction. If you want to implement a precise approach, consider the following type of tool.
- PickByValueExact Specifies the exact type of value to be extracted
/** * Core implementation */
type PickByValueExact<T, V> = Pick<T,
TypeKeys<{[P in keyof T]: [T[P]] extends [V]
? ([V] extends [T[P]] ? P : never)
: never; } > >// type Eg1 = { b: number };
type Eg1 = PickByValueExact<{a: string.b: number}, number>
// type Eg2 = { b: number; c: number | undefined }
type Eg2 = PickByValueExact<{a: string.b: number.c: number | undefined}, number>
Copy the code
The core implementation of PickByValueExact has three main points:
One is to use Pick to extract the type of key we need
The other is to circumvent the distributive union type of extends by giving generics a layer of tuples
The third is to use the two types compatible with each other to determine whether the same.
See the following example:
type Eq1<X, Y> = X extends Y ? true : false;
type Eq2<X, Y> = [X] extends [Y] ? true : false;
type Eq3<X, Y> = [X] extends [Y]
? ([Y] extends [X] ? true : false)
: false;
// Boolean, the expectation is false
type Eg1 = Eq1<string | number.string>
// false
type Eg2 = Eq2<string | number.string>
// True, the expectation is false
type Eg3 = Eq2<string.string | number>
// false
type Eg4 = Eq3<string.string | number>
// True, not in strictNullChecks mode
type Eg5 = Eq3<number | undefined.number>
// false, strictNullChecks mode results
type Eg6 = Eq3<number | undefined.number>
Copy the code
- from
Eg1
andEg2
As can be seen from the comparison, hereextends
Tuples of parameters can avoid the characteristics of distribution and get the desired results. - from
Eg3
andEg4
It can be seen from the comparison that the correct equality judgment of dependent types can be obtained by judging whether the two types are compatible with each other. - from
Eg5
andEg6
The comparison shows that notstrictNullChecks
Mode, undefined and NULL can be assigned to other types of features, resulting innumber | undefined, number
It’s compatible because it’s notstrictNullChecks
Pattern, so this result is expected. If this compatibility result is not required, it can be turned on completelystrictNullChecks
Mode.
Finally, the idea of getting OmitByValue and OmitByValueExact is not much to say. You can think about it yourself.
Intersection
Intersection
Extracts the key and corresponding type that exist in U from T. (Note that the key and type are finally extracted from T)
/** * select * from key */
type Intersection<T extends object, U extends object> = Pick<T,
Extract<keyof T, keyof U> & Extract<keyof U, keyof T>
>
type Eg = Intersection<{key1: string}, {key1:string.key2: number} >Copy the code
- The constraint
T
andU
Are allobject
And then usePick
Extract specifiedkey
Type of composition - through
Extract<keyof T, keyof U>
Extract the key that exists in both T and U,Extract<keyof U, keyof T>
It’s the same operation
So why do two extracts and then cross types? Reason lies in dealing with types of compatible derivation, remember the string can be assigned to a string | number compatible.
Extension:
Define Diff
to exclude keys and types that exist in U from T.
type Diff<T extends object, U extends object> = Pick<
T,
Exclude<keyof T, keyof U>
>;
Copy the code
Overwrite and Assign
Overwrite
Overwrites the property type of the same name in T from the property type of the same name in U. (The namesake property in the latter overrides the former)
/** * Overwrite implements * take the key and type that are unique to the former, take the key and type that are common to both, and merge them. * /
type Overwrite<
T extends object,
U extends object,
I = Diff<T, U> & Intersection<U, T>
> = Pick<I, keyof I>;
/ * * *@example* type Eg1 = { key1: number; } * /
type Eg1 = Overwrite<{key1: string}, {key1: number.other: boolean} >Copy the code
- The first constraint
T
andU
These are both parametersobject
- Using a default value for the parameter I as the implementation, no need to pass the parameter I (only auxiliary implementation)
- through
Diff<T, U>
Obtain to exist inT
But it doesn’t exist inU
And its type. (that is, to obtainT
Their own specialkey
And type). - through
Intersection<U, T>
To obtainU
andT
A total of thekey
The key is already inU
Type in. Get the name of the latterkey
Already type. - Finally, through the combination of cross types, the curve to save the country to achieve the coverage operation.
Extension: How to implement an Assign
(similar to object.assign ()) for merge?
/ / implementation
type Assign<
T extends object,
U extends object,
I = Diff<T, U> & Intersection<U, T> & Diff<U, T>
> = Pick<I, keyof I>;
/ * * *@example
* type Eg = {
* name: string;
* age: string;
* other: string;
* }
*/
type Eg = Assign<
{ name: string; age: number; },
{ age: string; other: string; } >.Copy the code
Find the key and type unique to the former, then find the key and type unique to the latter, then find the key and type unique to the latter, and then merge them.
DeepRequired
DeepRequired
Converts T to required attributes. If T is an object, then the recursive object converts all keys to Required and the type to NonUndefined; If T is an array then recursively traversing the array sets each item to NonUndefined.
/** * For the guidance of deepreguidance */
type DeepRequired<T> = T extends(... args:any[]) = >any
? T
: T extends Array<any>? _DeepRequiredArray<T[number]>
: T extends object
? _DeepRequiredObject<T>
: T;
// Helper, recursively traversing the array to convert each item to mandatory
interface _DeepRequiredArray<T> extends Array<DeepRequired<NonUndefined<T>>> {}
// Helper, recursively traversing the object to convert each item to mandatory
type _DeepRequiredObject<T extends object> = {
[P inkeyof T]-? : DeepRequired<NonUndefined<T[P]>> }Copy the code
DeepRequired
usingextends
Determine if phi is a function or phiPrimitive
Of type, then return that type directly.- If it is an array type, use the
_DeepRequiredArray
Recurse and pass the union type of all the subtypes of the array, as follows:
type A = [string.number]
/ * * *@description * number of array index access, is all item types of joint type * type B = string | number * /
type B = A[number]
Copy the code
-
_DeepRequiredObject is an interface (defined as type) whose type is Array
; In this case, T recurses each item through deep GUIDANCE
. Before T is used, it is handled once by NonUndefined
to remove invalid types.
-
In the case of an object type, recursive traversal of the object is implemented with _DeepRequiredObject. _DeepRequiredObject is just a normal mapping type variable, and then adds -? The modifier is converted to the required type.
DeepReadonlyArray
DeepReadonlyArray
converts T to read-only, all keys to read-only if T is object, and arrays to read-only if T is an array. The whole process is deeply recursive.
/** * DeepReadonly implements */
type DeepReadonly<T> = T extends ((. args:any[]) = > any) | Primitive
? T
: T extends _DeepReadonlyArray<infer U>
? _DeepReadonlyArray<U>
: T extends _DeepReadonlyObject<infer V>
? _DeepReadonlyObject<V>
: T;
/** * utility type, construct a read-only array */
interface _DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
/** * tool type, construct a read-only object */
type _DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};
Copy the code
- Basic implementation principle and
DeepRequired
Same thing, but be carefulinfer U
Automatically derive the type of the array,infer V
Derive the type of the object.
UnionToIntersection
Convert the union type to the crossover type.
type UnionToIntersection<T> = (T extends any
? (arg: T) = > void
: never
) extends (arg: infer U) => void ? U : never
type Eg = UnionToIntersection<{ key1: string } | { key2: number} >Copy the code
T extends any ? (arg: T) => void : never
This expression must branch true to construct a contravariant union type(arg: T1) => void | (arg: T2) => void | (arg: Tn) => void
- Reuse the second
extends
Cooperate withinfer
Derive the type of U, but useinfer
rightThe properties of covariant types result in crossover types.
Refer to the content
- Ts website www.typescriptlang.org/docs/handbo…
- The utility – types github.com/piotrwitek/…
Reprint please indicate the author and source!