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

  • keyofIndexing 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
  • ifextendsThe 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,extendsWhen 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:

  • inferIf 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
  • inferIf 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 typeTAll properties on
  • ? :Setting the property to be optional
  • T[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 notingkeyof anyGet isstring | number | symbol
  • The reason is that type key can only be of typestring | number | symbol

Extensions: homomorphism and non-homomorphism. Underline!! Underline!! Underline!!

  • Partial,ReadonlyandPickAre 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:.

  • RecordIt 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!!

  • neverRepresents a type that does not exist
  • neverAfter association with other types, there is noneverthe
/**
 * @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 usingPickExtract the types of keys that we need
  • So it’s going to beOmit = 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 beExclude<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 isExclude<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
  • ParametersFirst constraint parametersTIt has to be a function, so(... args: any) => any>replaceFunctionIt’s ok
  • Implementation is judgmentTIs it a function type? If it is, use itinter PLet TS derive the parameter types of the functions and store the results in the typesPUp, or backnever;

Hit the key!! Hit the key!! Hit the key!!

  • interThe 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 PThe result is stored on type P for use.
  • interKeywords 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 parametersTIs the class that has the constructor. Notice there’s aabstractModifier, which I’ll show you in a second.
  • When implemented, judgeTIs a class that satisfies the constraintinfer PAutomatically 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
  • ExcludeThe second parameter isT & UWhat you get is a cross type for all types
  • ExcludeThe first argument isT | UThis 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 typeobject
  • By mapping typeK in keyof TIterate over all keys and pass firstNonUndefined<T[K]>filterT[K]forundefined | nullIf not, return never
  • ifT[K]Is a valid typeFunctionType, 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 value
  • T['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 turn
  • T[keyof T]Is access toTType type of all values;
  • neverAnd other types,neverIt doesn’t exist. Such as:never | number | stringIs equivalent tonumber | string

Knock the key again!! Knock the key again!! Knock the key again!!

  • nullandundefinedYou can assign to other types (except for strict assignment tests to start that type), so it is needed in the above implementationNonUndefinedAntecedent judgment.
  • NonUndefinedIn the implementation, only to judgeT extends undefinedIn fact, the two can be compatible with each other. So you switch toT extends nullorT extends null | undefinedIt’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-readonlyRepresented as aNot read-onlyOr can be understood as removeread-only. forread-onlyProperties and-readonlyTurned out to beNot read-only, and forNot read-onlyProperties and-readonlyafterNot 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 YIs the two types to be compared, and returns if they are equalAIs not equally returnedB.
  • IfEqualsThe basic skeleton of PI isType IfEquals<> = (parameter 1) extends (parameter 2)? A : BIf the type of argument 1 can be assigned to the type of argument 2, then returnAOtherwise 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 hereXandY) 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 examplereadonly.Optional attributeWait, 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
  • IfEqualsFinally, there is the use of 1 and 2 to assist judgment (grammatical level), and there is to giveAThe default value of isX.BThe 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: ParseMutableKeysImplementation logic

  • MutableKeysFirst 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 iskeyOtherwise, the type isneverAnd 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, throughPick<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
  • fromEg1andEg2As can be seen from the comparison, hereextendsTuples of parameters can avoid the characteristics of distribution and get the desired results.
  • fromEg3andEg4It 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.
  • fromEg5andEg6The comparison shows that notstrictNullChecksMode, undefined and NULL can be assigned to other types of features, resulting innumber | undefined, numberIt’s compatible because it’s notstrictNullChecksPattern, so this result is expected. If this compatibility result is not required, it can be turned on completelystrictNullChecksMode.

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 constraintTandUAre allobjectAnd then usePickExtract specifiedkeyType of composition
  • throughExtract<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 constraintTandUThese are both parametersobject
  • Using a default value for the parameter I as the implementation, no need to pass the parameter I (only auxiliary implementation)
  • throughDiff<T, U>Obtain to exist inTBut it doesn’t exist inUAnd its type. (that is, to obtainTTheir own specialkeyAnd type).
  • throughIntersection<U, T>To obtainUandTA total of thekeyThe key is already inUType in. Get the name of the latterkeyAlready 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
  • DeepRequiredusingextendsDetermine if phi is a function or phiPrimitiveOf type, then return that type directly.
  • If it is an array type, use the_DeepRequiredArrayRecurse 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 andDeepRequiredSame thing, but be carefulinfer UAutomatically derive the type of the array,infer VDerive 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 : neverThis expression must branch true to construct a contravariant union type(arg: T1) => void | (arg: T2) => void | (arg: Tn) => void
  • Reuse the secondextendsCooperate withinferDerive the type of U, but useinferrightThe 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!