There are many built-in utility types in TypeScript, and because they are globally available, they can be used directly without import. Now that we know the basic tool types, we know not only how TypeScript implements them using the basic types we’ve covered in previous lectures, but also how to make better use of them so that we don’t reinvent the wheel and can implement more complex types from them.
According to the scope of use, we can divide tool types into operation interface type, union type, function type, and string type, which are described one by one in the following.
Operation Interface Type
Partial
Partial utility types can make all properties of a type optional and the utility type returns all subsets of the given type.
type Partial<T> = {
[P inkeyof T]? : T[P]; }; interface Person {name: string; age? : number; weight? : number; } type PartialPerson = Partial<Person>;/ / equivalent tointerface PartialPerson { name? : string; age? : number; weight? : number; }Copy the code
In the example above, we used the mapping type to extract all the key values of the incoming type and set their values to optional.
Required
In contrast to the Partial utility type, the Required utility type can make all attributes of a given type mandatory,
type Required<T> = {
[P inkeyof T]-? : T[P]; }; type RequiredPerson = Required<Person>;/ / equivalent to
interface RequiredPerson {
name: string;
age: number;
weight: number;
}
Copy the code
The mapping type uses a – symbol after the key value, – and? Combined to remove optional attributes of the type, so that all attributes of a given type become required.
Readonly
Readonly Utility types can make all properties of a given type read-only, which means that properties of a given type cannot be reassigned,
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type ReadonlyPerson = Readonly<Person>;
/ / equivalent tointerface ReadonlyPerson { readonly name: string; readonly age? : number; readonly weight? : number; }Copy the code
After Readonly, the name, age, and weight attributes of ReadonlyPerson become Readonly read-only.
Pick
The Pick tool type can Pick a specified key from a given type and form a new type,
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type NewPerson = Pick<Person, 'name' | 'age'>;
/ / equivalent to
interface NewPerson {
name: string; age? : number; }Copy the code
The Pick utility type takes two generic parameters, the first T being the given parameter type, and the second being the key value to extract. Given the parameter type and the key value that needs to be extracted, we can easily implement the functionality of the Pick tool type by mapping the type.
Omit
In contrast to the Pick type, the Omit tool type functions to return a new type after the specified key has been removed,
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type NewPerson = Omit<Person, 'weight'>;
/ / equivalent to
interface NewPerson {
name: string; age? : number; }Copy the code
The Omit type implementation uses the Pick type described earlier. We know that the function of the Pick type is to select the specified attributes of the given type, so the function of the Omit tool type should be to select the attributes except the specified attributes, and the function of the Exclude tool type is to Exclude several attributes specified by the input parameter K from the joint type of the input parameter T attribute.
Tips: The tool types described in the operation Interface Types section all use mapping types. By mapping types, we can remap the attributes of the original type to form the desired type.
The joint type
Exclude
In introducing the Omit type implementation, we use the Exclude type. By using the Exclude type, we remove the specified attribute from all attributes of the interface. Therefore, the effect of Exclude is to remove the specified type from the union type.
type Exclude<T, U> = T extends U ? never : T;
type T = Exclude<'a' | 'b' | 'c'.'a'>; // => 'b' | 'c'
type NewPerson = Omit<Person, 'weight'>;
/ / equivalent to
type NewPerson = Pick<Person, Exclude<keyof Person, 'weight'> >;/ / which
type ExcludeKeys = Exclude<keyof Person, 'weight'>; // => 'name' | 'age'
Copy the code
The implementation of Exclude uses a conditional type. T is not returned if type T can be assigned to type U, otherwise it is returned, and we remove the specified type from the union type.
Looking back at the previous example of the NewPerson type, it makes sense. In ExcludeKeys, if the property of type Person is the property we want to remove, it is not returned, otherwise its type is returned.
Extract
The Extract type acts as the opposite of Exclude in that Extract is primarily used to Extract the specified type from the union type, similar to the Pick type in the operation interface type.
type Extract<T, U> = T extends U ? T : never;
type T = Extract<'a' | 'b' | 'c'.'a'>; // => 'a'
Copy the code
We found that the Extract type is equivalent to fetching the intersection of two union types.
In addition, we can implement a tool type that gets the intersection of interface types based on Extract,
type Intersect<T, U> = {
[K in Extract<keyof T, keyof U>]: T[K];
};
interface Person {
name: string; age? : number; weight? : number; } interface NewPerson {name: string; age? : number; } type T = Intersect<Person, NewPerson>;/ / equivalent to
type T = {
name: string; age? : number; };Copy the code
We used the Extract type to Extract the intersection of two interface type attributes and generated a new type using the mapping type.
NonNullable
NonNullable removes null or undefined from the union type. If you’re already familiar with conditional types, you should know how to implement NonNullable.
type NonNullable<T> = T extends null | undefined ? never : T;
// The same as Exclude
type NonNullable<T> = Exclude<T, null | undefined>;
type T = NonNullable<string | number | undefined | null>; // => string | number
Copy the code
If a type passed by NonNullable can be assigned to null or undefined, it is not returned; otherwise, its specific type is returned.
Record
The purpose of Record is to generate the interface type, and then we use the generic parameters passed in as the properties and values of the interface type, respectively.
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type MenuKey = 'home' | 'about' | 'more';
interface Menu {
label: string; hidden? : boolean; }const menus: Record<MenuKey, Menu> = {
about: { label: 'about' },
home: { label: 'home' },
more: { label: 'more'.hidden: true}};Copy the code
The Record type accepts two generic parameters: the first parameter as an attribute of the interface type, and the second parameter as an attribute value of the interface type.
Note that the implementation here qualifies the first generic parameter to inherit from keyof any.
In TypeScript, keyof any refers to properties that can be used as object keys,
type T = keyof any; // => string | number | symbol
Copy the code
Note: Currently, JavaScript only supports string, number, and symbol as key values of objects.
Function types
ConstructorParameters
ConstructorParameters can be used to obtain the construction parameters of constructors, whereas an implementation of the ConstructorParameters type requires the infer keyword to infer the type of the construction parameters.
The infer keyword can be thought of as simple pattern matching. Infer returns the matched type if the actual parameter type matches infer.
type ConstructorParameters<T extends new(... args: any) => any> = Textends new (
...args: infer P
) => any
? P
: never;
class Person {
constructor(name: string, age? : number) {}
}
type T = ConstructorParameters<typeof Person>; // [name: string, age?: number]
Copy the code
The ConstructorParameters generic takes a parameter and restricts the need for that parameter to implement the constructor. Thus, we match the construction parameters within the constructor and return them via the infer keyword. Therefore, you can see that line 11 matches both arguments to the Person constructor and returns a tuple type [string, number] to the type alias T.
Parameters
Parameters work like ConstructorParameters in that they can be used to take the Parameters of a function and return an ordered pair,
type Parameters<T extends(... args: any) => any> = Textends(... args: infer P) => any ? P : never; type T0 = Parameters<() = > void>; / / []
type T1 = Parameters<(x: number, y? : string) = > void>; // [x: number, y?: string]
Copy the code
The generic parameter limits the type passed in to satisfy the function type.
ReturnType
ReturnType is used to get the ReturnType of the function,
type ReturnType<T extends(... args: any) => any> = Textends(... args: any) => infer R ? R : any; type T0 = ReturnType<() = > void>; // => void
type T1 = ReturnType<() = > string>; // => string
Copy the code
The generic argument to ReturnType restricts the type passed in to the function type.
ThisParameterType
ThisParameterType can be used to get the this parameter type of the function.
The this parameter of a function was introduced in class 05 on function types,
type ThisParameterType<T> = T extends (this: infer U, ... args: any[]) => any ? U : unknown; type T = ThisParameterType<(this: Number, x: number) = > void>; // Number
Copy the code
In line 1 of the example above, because the first parameter of the function type declares the this parameter type, we can simply use the infer keyword to match and retrieve the this parameter type. In line 3 of the example, the type alias T yields the type Number.
ThisType
ThisType allows you to specify the type of this in an object literal. ThisType does not return the converted type. Instead, ThisType specifies the type of this using ThisType’s generic argument. Here’s an example:
Note: If you want to use this utility type, you need to enable the TypeScript configuration for noImplicitThis.
type ObjectDescriptor<D, M> = { data? : D; methods? : M & ThisType<D & M>;// Methods this is of type D & M
};
function makeObject<D.M> (desc: ObjectDescriptor<D, M>) :D & M {
let data: object = desc.data || {};
let methods: object = desc.methods || {};
return{... data, ... methods }as D & M;
}
const obj = makeObject({
data: { x: 0.y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx; // this => D & M
this.y += dy; // this => D & M,}}}); obj.x =10;
obj.y = 20;
obj.moveBy(5.5);
Copy the code
{x: number, y: number} & {moveBy(dx: number, dy: number): void}
ThisType utility types simply provide an empty generic interface that is only recognized by TypeScript in the context of object literals,
interface ThisType<T> {}
Copy the code
That is, this type acts like any empty interface.
OmitThisParameter
The OmitThisParameter utility type is primarily used to remove this from function types. If the function type passed in does not explicitly declare this, the original function type is returned.
type OmitThisParameter<T> = unknown extends ThisParameterType<T>
? T
: T extends(... args: infer A) => infer R ?(. args: A) = > R
: T;
type T = OmitThisParameter<(this: Number, x: number) = > string>; // (x: number) => string
Copy the code
In the example above, the implementation of ThisParameterType returns the unknown type if the type of this cannot be inferred from the generic parameter passed in. In the OmitThisParameter implementation, the first conditional statement returns the original type if the function argument passed does not have type this; Otherwise, infer retrieves the types of function parameters and return values, respectively, to construct a new function type without this and return this function type.
String type
Template string
TypeScript supports template string literals as of version 4.1. To that end, TypeScript also provides four built-in manipulation string types: Uppercase, Lowercase, Capitalize, and Uncapitalize.
// Convert string literals to uppercase letters
type Uppercase<S extends string> = intrinsic;
// Convert string literals to lowercase letters
type Lowercase<S extends string> = intrinsic;
// Convert the first letter of a string literal to uppercase
type Capitalize<S extends string> = intrinsic;
// Convert the first letter of a string literal to lowercase
type Uncapitalize<S extends string> = intrinsic;
type T0 = Uppercase<'Hello'>; // => 'HELLO'
type T1 = Lowercase<T0>; // => 'hello'
type T2 = Capitalize<T1>; // => 'Hello'
type T3 = Uncapitalize<T2>; // => 'hello'
Copy the code
The implementations of these four manipulation string literal utility types are calculated using JavaScript runtime string manipulation functions and do not support locale Settings. The following code is an actual implementation of the four string utility types.
function applyStringMapping(symbol: Symbol, str: string) {
switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
case IntrinsicTypeKind.Uppercase:
return str.toUpperCase();
case IntrinsicTypeKind.Lowercase:
return str.toLowerCase();
case IntrinsicTypeKind.Capitalize:
return str.charAt(0).toUpperCase() + str.slice(1);
case IntrinsicTypeKind.Uncapitalize:
return str.charAt(0).toLowerCase() + str.slice(1);
}
return str;
}
Copy the code
As you can see from the above code, the string conversion uses the JavaScript string toUpperCase and toLowerCase methods, instead of toLocaleUpperCase and toLocaleLowerCase. ToUpperCase and toLowerCase use the Unicode encoding default case conversion rules.
Summary and preview
In this tutorial we looked at utility classes for manipulating interface types, union types, functions, and strings.
When learning the implementation of these tool types, we found that they were all implemented based on mapping types, conditional types, and infer. With these three types of manipulation in hand, we are free to combine more tool types.
Question: The implementation of code based on the Exclude utility type, please analyze why it can Exclude specified members from the union type?