1. Type guard
{ const convertToUpperCase = (strOrArray: string | string[]) => { if (typeof strOrArray === 'string') { return strOrArray.toUpperCase(); } else if (Array.isArray(strOrArray)) { return strOrArray.map(item => item.toUpperCase()); }}}Copy the code
The purpose of a type guard is to trigger type narrowing. In fact, it can also be used to distinguish between different members of a set of types.
How do you distinguish union types
Common type guards include switch, literal identity, Typeof, Instanceof, in, and custom type guards
switch
{
const convert = (c: 'a' | 1) => {
switch (c) {
case 1:
return c.toFixed(); // c is 1
case 'a':
return c.toLowerCase(); // c is 'a'
}
}
const feat = (c: { animal: 'panda'; name: 'China' } | { feat: 'video'; name: 'Japan' }) => {
switch (c.name) {
case 'China':
return c.animal; // c is "{ animal: 'panda'; name: 'China' }"
case 'Japan':
return c.feat; // c is "{ feat: 'video'; name: 'Japan' }"
}
};
}
Copy the code
Literals are identical
const convert = (c: 'a' | 1) => {
if (c === 1) {
return c.toFixed(); // c is 1
} else if (c === 'a') {
return c.toLowerCase(); // c is 'a'
}
}
Copy the code
typeof
const convert = (c: 'a' | 1) => {
if (typeof c === 'number') {
return c.toFixed(); // c is 1
} else if (typeof c === 'string') {
return c.toLowerCase(); // c is 'a'
}
}
Copy the code
instanceof
{ class Dog { wang = 'wangwang'; } class Cat { miao = 'miaomiao'; } const getName = (animal: Dog | Cat) => { if (animal instanceof Dog) { return animal.wang; } else if (animal instanceof Cat) { return animal.miao; }}}Copy the code
in
const getName = (animal: Dog | Cat) => {
if ('wang' in animal) { // ok
return animal.wang; // ok
} else if ('miao' in animal) { // ok
return animal.miao; // ok
}
}
Copy the code
Custom type guards
const isDog = function (animal: Dog | Cat): animal is Dog { return 'wang' in animal; } const getName = (animal: Dog | Cat) => { if (isDog(animal)) { return animal.wang; }}Copy the code
How to distinguish an enumeration type?
{ enum A { one, two } enum B { one, two } const cpWithNumber = (param: A) => { if (param === 1) { // bad return param; } } const cpWithOtherEnum = (param: A) => { if (param === B.two as unknown as A) { // ALERT bad return param; } } const cpWithSelf = (param: A) => { if (param === A.two) { // good return param; }}}Copy the code
Invalid type guard
const getName = <T extends Dog | Cat>(animal: T) => { if ('wang' in animal) { return animal.wang; // ts(2339) } return animal.miao; // ts(2339) }; / / const getName = < T extends Dog | Cat > (animal: T) = > {the if (isDog (animal)) {/ / instanceOf can return animal. Wang; // ok } return (animal as Cat).miao; // ts(2339) };Copy the code
2. Type compatibility
Special case
any
The any type can be assigned to any type other than never, and vice versa. That is, any is compatible with all types except never, and is compatible with all types
never
The property of never is that it can be assigned to any other type, but in turn cannot be assigned to any other type, including any
unknown
The property of unknown is almost the reverse of the property of never, that is, we cannot assign unknown to any type other than any, and in turn any type can be assigned to unknown
Void, null, undefined
Void, null, and undefined are also particularly compatible. For example, void can only be assigned to any and unknown, whereas only any, never, and undefined can be assigned to void
enum
Numeric enumerations and numeric types are compatible. Different enumerations are incompatible
Type compatibility
subtypes
From the perspective of subtypes, all subtypes are compatible with their parent types
{
const one = 1;
let num: number = one; // ok
interface IPar {
name: string;
}
interface IChild extends IPar {
id: number;
}
let Par: IPar;
let Child: IChild;
Par = Child; // ok
class CPar {
cname = '';
}
class CChild extends CPar {
cid = 1;
}
let ParInst: CPar;
let ChildInst: CChild;
ParInst = ChildInst; // ok
let mixedNum: 1 | 2 | 3 = one; // ok
}
Copy the code
let ICPar: IPar | CPar;
let ICChild: IChild | CChild;
ICPar = ICChild; // ok
Copy the code
Structure type
Another criterion for type compatibility is structure type, which means that two types are compatible if they have the same structure. For example, interface types or classes that have attributes or methods of the same type can be assigned to each other.
{
class C1 {
name = '1';
}
class C2 {
name = '2';
}
interface I1 {
name: string;
}
interface I2 {
name: string;
}
let InstC1: C1;
let InstC2: C2;
let O1: I1;
let O2: I2;
InstC1 = InstC2; // ok
O1 = O2; // ok
InstC1 = O1; // ok
O2 = InstC2; // ok
}
Copy the code
Two interface types or classes are compatible if one of them not only has all of the attributes and methods of the other type, but also contains other attributes and methods (like subclasses derived from the other type).
{
interface I1 {
name: string;
}
interface I2 {
id: number;
name: string;
}
class C2 {
id = 1;
name = '1';
}
let O1: I1;
let O2: I2;
let InstC2: C2;
O1 = O2;
O1 = InstC2;
}
Copy the code
Although the variable O2 containing the redundant attribute ID can be assigned to the variable O1, if we directly assign an object literal of the exact structure of variable O2 to the variable O1, we will get a TS (2322) type incompatible error (line 2 in the following example), This is the freshness feature of object literals.
O1 = {
id: 2, // ts(2322)
name: 'name'
};
let O3 = {
id: 2,
name: 'name'
};
O1 = O3; // ok
O1 = {
id: 2,
name: 'name'
} as I2; // ok
Copy the code
Class compatibility features: In fact, when determining whether two classes are compatible, we can completely ignore the compatibility of their constructors and static properties and methods, and simply compare the compatibility of the properties and methods of the class instance. If two classes contain private, protected properties and methods, they are compatible only if the properties and methods originate from the same class.
{
class C1 {
name = '1';
private id = 1;
protected age = 30;
}
class C2 {
name = '2';
private id = 1;
protected age = 30;
}
let InstC1: C1;
let InstC2: C2;
InstC1 = InstC2; // ts(2322)
InstC2 = InstC1; // ts(2322)
}
{
class CPar {
private id = 1;
protected age = 30;
}
class C1 extends CPar {
constructor(inital: string) {
super();
}
name = '1';
static gender = 'man';
}
class C2 extends CPar {
constructor(inital: number) {
super();
}
name = '2';
static gender = 'woman';
}
let InstC1: C1;
let InstC2: C2;
InstC1 = InstC2; // ok
InstC2 = InstC1; // ok
}
Copy the code
Inheritable and implementable
Type compatibility also determines whether interface types and classes can inherit from another interface type or class through extends, and whether classes can implement interfaces through implements.
{ interface I1 { name: number; } interface I2 extends I1 { // ts(2430) name: string; } class C1 { name = '1'; private id = 1; } class C2 extends C1 { // ts(2415) name = '2'; private id = 1; } class C3 implements I1 { name = ''; // ts(2416)}} // Interface type I1 and interface type I2 contain different types of name attribute incompatible, so interface type I2 cannot inherit interface type I1. // Classes C1 and C2 do not meet the class compatibility conditions, so class C2 cannot inherit from class C1. Interface type I1 and class C3 contain different types of name attributes, so class C3 cannot implement interface type I1.Copy the code
The generic
The compatibility of generic types, generic classes, really refers to the compatibility of instantiating them into an exact type.
variant
Infer subtype relationships between more complex types based on them based on the subtype relationships between types.
Use the expression of a function in mathematics. For example, the Dog type, we can use F(Dog) to represent a constructed complex type; F(Animal) represents a complex type based on Animal construction.
- (1) Covariation
{ type isChild<Child, Par> = Child extends Par ? true : false; interface Animal { name: string; } interface Dog extends Animal { woof: () => void; } type Covariance<T> = T; type isCovariant = isChild<Covariance<Dog>, Covariance<Animal>>; // Covariant<Dog> is a subtype of Covariant<Animal>, so isCovariant is true.Copy the code
- (2) Invert
Contravariant means that if Dog is a child of Animal, then F(Dog) is the parent of F(Animal), which is the opposite of covariant.
In the TypeScript rigor we prefer, function parameter types are contravariant,
type Contravariance<T> = (param: T) => void;
type isNotContravariance = isChild<Contravariance<Dog>, Contravariance<Animal>>; // false;
type isContravariance = isChild<Contravariance<Animal>, Contravariance<Dog>>; // true;
Copy the code
- (3) Bidirectional covariance
Bidirectional covariant means that if Dog is a subtype of Animal, then F(Dog) is a subtype and parent type of F(Animal), both covariant and contravariant.
In real life, in TypeScript non-strict mode, function parameter types are bidirectional covariant. As mentioned earlier, functions are only safe if their parameters are contravariant, and this course has been emphasizing the use of strict modes, so bidirectional covariance is not a safe or useful feature, so we are unlikely to encounter such a practical scenario.
- (4) unchanged
Immutable means that as long as they are not exactly the same type, they must be incompatible. So even if Dog is a subtype of Animal, if F Dog is not a subtype of F Animal, then F Animal is not a subtype of F Dog.
interface Cat extends Animal { miao: () => void; } const cat: Cat = { name: 'Cat', miao: () => void 0, }; const dog: Dog = { name: 'Dog', woof: () => void 0, }; let dogs: Dog[] = [dog]; animals = dogs; // ok animals.push(cat); // ok dogs.forEach(visitDog); // Type OK, but an error is thrown at runtimeCopy the code
For mutable arrays, immutable seems like a safer and more reasonable setup. However, mutable and immutable arrays are covariant in TypeScript, which is a pitfall to be aware of.
Function parameter types are contravariant and return value types are covariant
Function type compatibility
(1) Return value
We have already said that return value types are covariant, so the subtype relationship of a function is the same as the subtype relationship of the return value, provided that the parameter types are compatible. That is, if the return value type is compatible, the function is compatible.
(2) Parameter type
We also mentioned that parameter types are contravariant, so in the case of the same number of parameters and compatible return value types, the relationship between function subtype and parameter subtype is reversed (contravariant).
(3) Number of parameters
Function compatibility depends on the number of parameters in the same index position and the compatibility of return value types
{
let lessParams = (one: number) => void 0;
let moreParams = (one: number, two: string) => void 0;
lessParams = moreParams; // ts(2322)
moreParams = lessParams; // ok
}
Copy the code
(4) Optional and residual parameters
Optional parameters are compatible with other parameters and non-optional parameters.
let optionalParams = (one? : number, tow? : number) => void 0; let requiredParams = (one: number, tow: number) => void 0; let restParams = (... args: number[]) => void 0; requiredParams = optionalParams; // ok restParams = optionalParams; // ok optionalParams = restParams; // ts(2322) optionalParams = requiredParams; // ts(2322) restParams = requiredParams; // ok requiredParams = restParams; // okCopy the code
3. Enhance the type system
How do you safely use JavaScript libraries in TypeScript? The key step is to use a DECLARE keyword in TypeScript.
Using the DECLARE keyword, we can declare global variables, methods, classes, and objects.
Declare a variable
At run time, front-end code
Declare variables of syntax: declare (var | let | const) variable name: variable types
declare var val1: string;
declare let val2: number;
declare const val3: boolean;
val1 = '1';
val1 = '2';
val2 = 1;
val2 = '2'; // TS2322: Type 'string' is not assignable to type 'number'.
val3 = true; // TS2588: Cannot assign to 'val3' because it is a constant.
Copy the code
Declare functions
The syntax for declaring functions is the same as that for declaring variable types, except that the declare keyword is followed by the function keyword
declare function toString(x: number): string;
const x = toString(1); // => string
Copy the code
When using the DECLARE keyword, we do not need to write concrete implementations of declared variables, functions, and classes (because variables, functions, and classes are already implemented in other libraries), but simply declare their types
// TS1183: An implementation cannot be declared in ambient contexts. declare function toString(x: number) { return String(x); }; // TypeScript error messages indicate that context declarations do not need to be implemented. That is, all types declared by declare only need to indicate the type, not the implementation.Copy the code
Class declarations
To declare a class, we only need to declare the type of the class’s properties and methods.
declare class Person {
public name: string;
private age: number;
constructor(name: string);
getAge(): number;
}
const person = new Person('Mike');
person.name; // => string
person.age; // TS2341: Property 'age' is private and only accessible within class 'Person'.
person.getAge(); // => number
Copy the code
The statement enumeration
To declare an enumeration, you define only the type of the enumeration, not the value of the enumeration
declare enum Direction {
Up,
Down,
Left,
Right,
}
const directions = [Direction.Up, Direction.Down, Direction.Left, Direction.Right];
Copy the code
Note: Declaration enumerations are used only as a compile-time check, after compilation, the contents of the declaration file are removed from the compilation results
Declare the module
Declare module syntax: Declare Module ‘module name’ {}.
// lodash.d.ts
declare module 'lodash' {
export function first<T extends unknown>(array: T[]): T;
}
// index.ts
import { first } from 'lodash';
first([1, 2, 3]); // => number;
Copy the code
The declaration file is a file with the suffix.d.ts. Inside the module declaration, we only need to use export to export the corresponding library classes and functions.
Declare file
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
Copy the code
declare namespace
Unlike declarative modules, namespaces are used to represent global object variables with many child properties or methods
declare namespace $ { const version: number; function ajax(settings? : any): void; } $.version; // => number $.ajax(); // Declare the globally imported jQuery variable $, so you can directly use the version attribute of the $variable and the Ajax methodCopy the code
Declaration file
Files with the suffix.d.ts are declaration files. When declaring the file, we only need to define the API interface exposed by the tripartite library
type
-
Type alias declaration;
-
Interface declaration;
-
Class declaration;
-
Enumeration declaration;
-
Type declaration for the import.
Each of the above declarations creates a type name.
value
A value is a value that an expression can assign at run time.
You can create a value in the following six ways:
-
Var, let, const declarations;
-
Declarations of namespace and Module containing values;
-
Enumeration declaration;
-
Class declaration;
-
Imported values;
-
Function declaration.
The namespace
We can also declare types in namespaces. For example, the const x: A.B.C declaration, where type C is in the A.B namespace.
Use declaration files
After installing TypeScript dependencies, we typically install a lib.d.ts declaration file that contains declarations of various global variables in the JavaScript runtime and DOM
// typescript/lib/lib.d.ts
/// <reference no-default-lib="true"/>
/// <reference lib="es5" />
/// <reference lib="dom" />
/// <reference lib="webworker.importscripts" />
/// <reference lib="scripthost" />
Copy the code
Using the @ types
Definitely Typed is the most popular high-quality TypeScript declaration file library
Search here for the type declaration of the library you want to import
Problem: Because it is maintained by the community, if the original partite library is upgraded, the type definitions of the exported partite library need to go through PR and publication processes before being upgraded. As a result, it cannot be completely synchronized with the original library. In TypeScript, we can solve this problem temporarily by combining types and extending type definitions.
Type of merger
In TypeScript, identical interfaces and namespaces are merged according to certain rules.
Combined interface
The simplest and most common declaration merge is interface merge
interface Person { name: string; } interface Person { age: number; } // equivalent to interface Person {name: string; age: number; } // It is important to note that non-function members of the interface should be type compatibleCopy the code
For function members, each function declaration with the same name is treated as an overload of that function. The interface declared later has higher priority
interface Obj { identity(val: any): any; } interface Obj { identity(val: number): number; } interface Obj { identity(val: boolean): boolean; } // equivalent to interface Obj {identity(val: Boolean): Boolean; identity(val: number): number; identity(val: any): any; } const obj: Obj = { identity(val: any) { return val; }}; const t1 = obj.identity(1); // => number const t2 = obj.identity(true); // => boolean const t3 = obj.identity("t3"); // => anyCopy the code
Merge the namespace
Merging namespaces is similar to merging interfaces in that namespace merging also merges the attributes of its exported members. The difference is that non-exported members are visible only within the original namespace.
namespace Person {
const age = 18;
export function getAge() {
return age;
}
}
namespace Person {
export function getMyAge() {
return age; // TS2304: Cannot find name 'age'.
}
}
Copy the code
Do not merge
For classes, the special object that is both a value and a type cannot be merged.
Expansion module
// person.ts export class Person {} // index.ts import { Person } from './person'; declare module './person' { interface Person { greet: () => void; } } Person.prototype.greet = () => { console.log('Hi! '); }; // Declare the Person attribute in the imported module Person. TypeScript merges the type of the original module. This way we can extend the type of the imported module. We also added the greet method on the prototype chain to the imported Person class.Copy the code
// person.ts export class Person {} // index.ts import { Person } from './person'; declare module './person' { interface Person { greet: () => void; } } - declare module './person' { - interface Person { - greet: () => void; - } - } + // TS2339: Property 'greet' does not exist on type 'Person'. Person.prototype.greet = () => { console.log('Hi! '); }; [Error] ts(2339) does not exist greet attribute. [error] Ts (2339) does not exist greet attribute. // For imported tripartite modules, we can also use this method to extend the properties of the original module.Copy the code
Expansion of the global
Global modules refer to modules that can be used without import, such as global window, document, etc.
Extending global objects is the same as extending modules
declare global { interface Array<T extends unknown> { getLen(): number; } } Array.prototype.getLen = function () { return this.length; }; // Global Array objects are declared to have a getLen method, so TypeScript does not error when implementing getLen methods for Array objects.Copy the code
4. Master the official tool types
Operation Interface Type
Partial
Partial utility types can make all attributes of a type optional and the utility type returns all subsets of the given type
type Partial<T> = { [P in keyof T]? : T[P]; }; interface Person { name: string; age? : number; weight? : number; } type PartialPerson = Partial<Person>; // Equivalent to interface PartialPerson {name? : string; age? : number; weight? : number; }Copy the code
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 in keyof 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 attributes of a given type read-only, which means that attributes of a given type cannot be reassigned
type Readonly<T> = { readonly [P in keyof T]: T[P]; }; type ReadonlyPerson = Readonly<Person>; Interface 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 utility type can Pick the specified key value from a given type and compose a new type
type Pick<T, K extends keyof T> = { [P in K]: T[P]; }; type NewPerson = Pick<Person, 'name' | 'age'>; Interface NewPerson {name: string; age? : number; }Copy the code
Omit
In contrast to the Pick type, the Omit tool type functions to return a new type after the specified key value has been removed
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; type NewPerson = Omit<Person, 'weight'>; Interface NewPerson {name: string; age? : number; }Copy the code
All tool types that operate on interface types 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'>; NewPerson = Pick<Person, Exclude<keyof Person, 'weight'>>; // 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.
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
The Extract type is equivalent to fetching the intersection of two union types.
You 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>; Type T = {name: string; age? : number; };Copy the code
NonNullable
NonNullable removes null or undefined from the union type
type NonNullable<T> = T extends null | undefined ? never : T; / / equivalent to use Exclude type NonNullable < T > = Exclude < T, null | undefined >; type T = NonNullable<string | number | undefined | null>; // => string | numberCopy the code
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 implementation here qualifies that the first generic parameter inherits 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
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> = T extends 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
Parameters
Parameters function 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> = T extends (... 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
ReturnType
ReturnType is used to get the ReturnType of the function
type ReturnType<T extends (... args: any) => any> = T extends (... args: any) => infer R ? R : any; type T0 = ReturnType<() => void>; // => void type T1 = ReturnType<() => string>; // => stringCopy the code
ThisParameterType
ThisParameterType can be used to get the this parameter type of the function.
type ThisParameterType<T> = T extends (this: infer U, ... args: any[]) => any ? U : unknown; type T = ThisParameterType<(this: Number, x: number) => void>; // NumberCopy the code
ThisType
ThisType allows you to specify the type of this in an object literal. ThisType does not return the converted type, but rather specifies the type of this via ThisType’s generic argument
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
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) => stringCopy the code
String type
Template string
TypeScript supports template string literals as of version 4.1. To this end, TypeScript also provides four built-in action string types: Uppercase, Lowercase, Capitalize, and Uncapitalize
Type Uppercase<S extends String > = intrinsic; Lowercase<S extends String > = intrinsic; // Convert the first letter of a string literal to Capitalize 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
5. Type programming: Create your own tool type
Essentially a custom complex type constructor (generics, to be exact)
The generic
type isSubTying<Child, Par> = Child extends Par ? true : false;
type isXX2 = isSubTyping<1, number>; // true
type isYY2 = isSubTyping<'string', string>; // true
type isZZ2 = isSubTyping<true, boolean>; // true
Copy the code
Conditions in the
The type of condition for a ternary operation
TypeScript supports conditional types that use ternary operations, and ternary operations also support nesting
type isSubTyping<Child, Par> = Child extends Par ? true : false;
type isAssertable<T, S> = T extends S ? true : S extends T ? true : false;
type isNumAssertable = isAssertable<1, number>; // true
type isStrAssertable = isAssertable<string, 'string'>; // true
type isNotAssertable = isAssertable<1, boolean>; // false
Copy the code
Allocation condition type
If the input parameter is a union type, it is broken down into separate (atomic) types (members) and then type operations are performed.
type BooleanOrString = string | boolean; type StringOrNumberArray<E> = E extends string | number ? E[] : E; type WhatIsThis = StringOrNumberArray<BooleanOrString>; // boolean | string[] type BooleanOrStringGot = BooleanOrString extends string | number ? BooleanOrString[] : BooleanOrString; / / string | Boolean / / BooleanOrString being treated as a whole, so BooleanOrStringGot is of type string | Boolean.Copy the code
It is also possible to unassign a type by forcing it to be treated as a whole
type StringOrNumberArray<E> = [E] extends [string | number] ? E[] : E; type WhatIsThis = StringOrNumberArray<string | boolean>; / / string | using Boolean / / [] will into E wrapped up, even if the parameter is joint type string | Boolean, will also be treated as a whole, so returns the string | Boolean.Copy the code
Note: There is a “pitfall” when generics that contain conditional types accept never as their input parameter
type GetSNums = never extends number ? number[] : never extends string ? string[] : never; // number[];
type GetNever = StringOrNumberArray<never>; // never
Copy the code
Never is an underlying type that cannot be assigned, and if it occurs atomically as an input parameter to the left of the extends keyword, then the instantiated type is also Never.
type UsefulNeverX<T> = T extends {} ? T[] : [];
type UselessNeverX<T, S> = S extends {} ? S[] : [];
type UselessNeverY<T, S> = S extends {} ? T[] : [];
type UselessNeverZ<T> = [T] extends [{}] ? T[] : [];
type ThisIsNeverX = UsefulNeverX<never>; // never
type ThisIsNotNeverX = UselessNeverX<never, string>; // string[]
type ThisIsNotNeverY = UselessNeverY<never, string>; // never[]
type ThisIsNotNeverZ = UselessNeverZ<never>; // never[]
Copy the code
Type infer from conditional types
Use the type inference operator from a conditional type to infer the components of a type input parameter, such as the type of the element in an array type input parameter.
{
type ElementTypeOfArray<T> = T extends (infer E)[] ? E : never;
type isNumber = ElementTypeOfArray<number[]>; // number
type isNever = ElementTypeOfArray<number>; // never
}
Copy the code
Infer can also be used to derive arbitrary member types by creating arbitrary type inference parameters
{
type ElementTypeOfObj<T> = T extends { name: infer E; id: infer I } ? [E, I] : never;
type isArray = ElementTypeOfObj<{ name: 'name'; id: 1; age: 30 }>; // ['name', 1]
type isNever = ElementTypeOfObj<number>; // never
}
Copy the code
Index access type
The index access type is actually more like a way of retrieving materials. First, we can extract the type of any member of the object (interface type) on demand by attribute name, index, index signature (note: only [index name] syntax is used).
interface MixedObject {
animal: {
type: 'animal' | 'dog' | 'cat';
age: number;
};
[name: number]: {
type: string;
age: number;
nickname: string;
};
[name: string]: {
type: string;
age: number;
};
}
type animal = MixedObject['animal'];
type animalType = MixedObject['animal']['type'];
type numberIndex = MixedObject[number];
type numberIndex0 = MixedObject[0];
type stringIndex = MixedObject[string];
type stringIndex0 = MixedObject['string'];
Copy the code
keyof
You can use the keyof keyword to extract the object attribute name, index name, and type of index signature
type MixedObjectKeys = keyof MixedObject; // string | number
type animalKeys = keyof animal; // 'type' | 'age'
type numberIndexKeys = keyof numberIndex; // "type" | "age" | "nickname"
Copy the code
typeof
If we use Typeof in an expression context, it is the type used to get expression values, and if we use typeof in a type context, it is the type used to get variables or attributes. Of course, in TypeScript, typeof’s primary purpose is to get the typeof a variable or property in a type context
{ let StrA = 'a'; const unions = typeof StrA; / / unions type is "string" | "number" | "bigint" | "Boolean" | "symbol" | "undefined" | "object" | "function" const STR: typeof StrA = 'string'; // The STRS type is string type DerivedFromStrA = typeof StrA; // string }Copy the code
Mapping type
You can use index signature syntax and the IN keyword to limit the scope of object attributes
type SpecifiedKeys = 'id' | 'name';
type TargetType = {
[key in SpecifiedKeys]: any;
}; // { id: any; name: any; }
type TargetGeneric<O extends string | number | symbol> = {
[key in O]: any;
}
type TargetInstance = TargetGeneric<SpecifiedKeys>; // { id: any; name: any; }
Copy the code
Note: We can only use in in type alias definitions. If we use in interfaces, we will be prompted with a TS (1169) error
interface ITargetInterface {
[key in SpecifiedKeys]: any; // ts(1169)
}
Copy the code
When defining a type, we can use a combination of in and keyof, and create a new type based on an existing type. Read-only, optional features that make the new type conform to the existing type are called mapped types.
Note: In and keyof can also only be used together in type alias definitions.
interface SourceInterface { readonly id: number; name? : string; } type TargetType = { [key in keyof SourceInterface]: SourceInterface[key]; }; // { readonly id: number; name? : string | undefined } type TargetGenericType<S> = { [key in keyof S]: S[key]; }; type TargetInstance = TargetGenericType<SourceInterface>; // { readonly id: number; name? : string | undefined }Copy the code
You can use readonly,? Modifiers are used to describe readability and optionability of attributes. You can also add or remove specified modifiers by prefixing them with + and – (default: + and Add).
type TargetGenericTypeReadonly<S> = { readonly [key in keyof S]: S[key]; } type TargetGenericTypeReadonlyInstance = TargetGenericTypeReadonly<SourceInterface>; // { readonly id: number; readonly name? : string | undefined } type TargetGenericTypeOptional<S> = { [key in keyof S]? : S[key]; } type TargetGenericTypeOptionalInstance = TargetGenericTypeOptional<SourceInterface>; // { readonly id? : number; readonly name? : string | undefined } type TargetGenericTypeRemoveReadonly<S> = { -readonly [key in keyof S]: S[key]; } type TargetGenericTypeRemoveReadonlyInstance = TargetGenericTypeRemoveReadonly<SourceInterface>; // { id: number; name? : string | undefined } type TargetGenericTypeRemoveOptional<S> = { [key in keyof S]-? : S[key]; } type TargetGenericTypeRemoveOptionalInstance = TargetGenericTypeRemoveOptional<SourceInterface>; // { readonly id: number; name: string }Copy the code
Remap keys using AS
Since TypeScript 4.1, we can use type assertions in index signatures for mapped types
type TargetGenericTypeAssertiony<S> = { [key in keyof S as Exclude<key, 'id'>]: S[key]; } type TargetGenericTypeAssertionyInstance = TargetGenericTypeAssertiony<SourceInterface>; // { name? : string | undefined; }Copy the code
To build the wheels
Exclude
type ExcludeSpecifiedNumber = Exclude<1 | 2, 1>; // 2
type ExcludeSpecifiedString = Exclude<'id' | 'name', 'id'>; // 'name
type ExcludeSpecifiedBoolean = Exclude<boolean, true>; // false
Copy the code
The code implementation is as follows:
type Exclude<T, U> = T extends U ? never : T;
Copy the code
ReturnTypeOfResolved
The difference between ReturnTypeOfResolved and official ReturnType: if the ReturnType of input parameter F is an instance of a generic Promise, return the input that the Promise received.
// type ReturnType<T extends (... args: any) => any> = T extends (... args: any) => infer R ? R : any; type ReturnTypeOfResolved<F extends (... args: any) => any> = F extends (... args: any[]) => Promise<infer R> ? R : ReturnType<F>; type isNumber = ReturnTypeOfResolved<() => number>; // number type isString = ReturnTypeOfResolved<() => Promise<string>>; // stringCopy the code
Merge
A generic Merge<A, B> that merges type inputs A and B into one type based on the mapping type
type Merge<A, B> = {
[key in keyof A | keyof B]: key extends keyof A
? key extends keyof B
? A[key] | B[key]
: A[key]
: key extends keyof B
? B[key]
: never;
};
type Merged = Merge<{ id: number; name: string }, { id: string; age: number }>;
Copy the code
Equal
Custom utility type Equal<S, T>, which can be used to determine whether input arguments S and T are of the same type. Boolean literal type true if the same, false otherwise.
Other series of articles
A how-to guide to TypeScript