Typescript type inference is a very interesting process where you play a game of inference. In this game, the protagonist is you, a genius detective, good at reasoning; After analyzing the type information, the final type can be obtained through operation on the type.
At this point, you’re assuming you already have some understanding of typescript basics such as primitive type declarations, generics, and type aliases. How not, you can look at typescript to lay the foundation and go.. . Type derivation is an evolutionary process, and type operations make it very flexible. Before you begin, keep in mind that operations on types provide endless possibilities for type expression
Type Operation Basics
Typescript documentation mentions two Types: Unions and Intersection Types. Through two symbols |, & to implement. There’s a very important term in type derivation called satisfied, which means that type derivation works, meets the minimum requirements for type constraints. When a type error occurs, you often see the compiler prompt: ‘type X’ is not assignable to type Y’, as in ‘number’ is not assignable to type ‘string’. Numeric types cannot be assigned to string types.
Whether satisfied or assignable, they are both a result of type derivation, equivalent to True or False, there is no real difference, except that sometimes it is more natural to use one than the other in type derivation, conforming to some linguistic conventions.
Union/cross type
The union/cross type is actually a simpler concept, so let’s compare:
- Unions Type: Meet one of the constraints on the type
- Cross type: all constraints on the type must be met
// Union type
type Odd = 1 | 3 | 5;
// 1 is one of (1, 3, 5) and satisfies one of the type constraints
const a: Odd = 1;
Type '2' is not assignable to Type 'Odd'
// Because 2 does not satisfy any of the type constraints
const b: Odd = 2;
// Cross types
A: / / conditions
interface A {
str: string;
}
B / / conditions
interface B {
num: number;
}
// both conditions AB are met
type AB = A & B;
/** * Both types of constraints * Condition 1: STR is a string * Condition 2: num is a number */
const Both: AB = {
str: "hello world".num: 0};Copy the code
|, & type of operation, is actually to constraints or, and the relationship between the processing. In business development, generic type parameter constraints may be used more often, combining multiple types:
/ / state
type MediaType = "Text" | "Image" | "Video" | "Audio";
// Public attributes of the media
// The type argument T inherits from MediaType. The default value is Video
// that is, T is content with MediaTyped type constraints
interface Media<T extends MediaType = "Video"> {
/ / identifier
id: string;
/ / name
name: string;
// Media type
type: T;
}
type VideoMedia = Media & {
// Playback duration
duration: number;
};
const video: VideoMedia = {
id: 0.name: "I am a video".type: "Video".duration: 120};Copy the code
The join/cross type can be understood not only in terms of defining a type, but also in terms of operations. It is more interesting to understand that a type operator, like addition, subtraction, multiplication, and division, is an inference constraint operation.
The operator
TypeScript’s type system is powerful because it allows types to be expressed in terms of other types. With various combinations of type operators, we can express complex operations and values in a concise, maintainable manner.
Keyof operator.
Keyof acts on an object type (mapped types) and generates joint types of strings or numbers from its keys.
type Point = { x: number; y: number };
/ / to extract key "x" | "y"
type Props = keyof Point;
type Mapish = { [k: string] :boolean };
/ * * * is extracted to string | number; * Because in javascripte obj[0] is converted to obj["0"] */
type Keys = keyof Mapish;
Copy the code
The index signature above involves type compatibility issues, and it is worth explaining why. TypeScript supports two index signatures: strings and numbers. Both types of indexes can be used, but the return value of the numeric index must be a subtype of the return value type of the string index. This is because when you index a number, JavaScript converts it to a string and then indexes the object. That is, indexing with 100 (a number) is the same as indexing with 100 (a string), so the two need to be consistent.
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// Error: Use numeric string index, sometimes get completely different Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
Copy the code
The typeof operator
TypeScript adds a typeof operator that you can use in a type context to reference the typeof a variable or property.
- The object typeof functions on is a javascript value, not a type
- For a javascript value, using typeof deduces the typeof the value.
let a = { id: 0.name: "hello world" };
// Deduce that the type of variable a is {id: number; name: string; }
let b: typeof a = {
id: 0.name: ""};interface A {
id: string;
}
Typeof cannot operate on a type
type C = typeof A;
Copy the code
Index access
We can access attributes of a type by indexing them
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];
const MyArray = [
{ name: "Alice".age: 15 },
{ name: "Bob".age: 23 },
{ name: "Eve".age: 38},];{name: string; age: number; }
type ListItem = typeof MyArray[number];
type AgeType = typeof MyArray[number] ["age"];
const key = "name";
// It refers to a value, but is being used as a type here
// Key must be a type
type NameType = Person[key];
// Successful derivation
type A = Person[typeof key];
Copy the code
Object Object type
- In operator:
[Property in keyof Type]
Restricting keys to a collection is the union type extracted by keyof - Remove/Add modifiers: Pass
-
or+
, remove/add modifiers (mainly: readonly,?) - Rename key name: To rename a key name, a feature to be installed after 4.1
JavaScript and TypeScript Nightly
- Operation on key type: Type operation is performed with key as type parameter
/// in operator
// [Property in keyof Type] The index signature indicates that each key is derived from the Type parameter Type
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
type FeatureFlags = {
darkMode: () = > void;
newUserProfile: () = > void;
};
DarkMode: Boolean; newUserProfile: boolean; }; * /
type FeatureOptions = OptionsFlags<FeatureFlags>;
// Remove readonly, optional - modifier
type RemoveModifiers<Type> = {
-readonly [Property inkeyof Type]-? : Type[Property]; };type MaybeUser = {
readonlyid? :string; name? :string; age? :number;
};
// 运算结果: { id: string; name: string; age: number; }
type MustUser = RemoveModifiers<MaybeUser>;
type RenameMapKey<Type> = {
[Property in keyof Type as `re_The ${string & Property}`]: Type[Property];
};
{readonly re_id? : string; re_name? : string; re_age? : number; }
// It is obvious that each of the original keys is prefixed with re
type RenameUser = RenameMapKey<MaybeUser>;
type KeyMoreOperator<Type> = {
// Keyof retrieves the key, accessed through the Type[Property] index, and you can perform any Type operation
// This is just a primer
[Property in keyof Type]: Type[Property] extends { [p: string] :number[]}?string
: "others";
};
Copy the code
Template character type
The template literal type builds on the string literal type and can be extended to multiple strings through federation.
Use text types in interpolation
type Str = "a" | "b" | "c";
type Num = 0 | 1 | 2;
//
type StrNum = `${Str | Num}`;
type Str_Num = Str | Num;
Type error because StrNum is a template string type
/ / 0 | 1 | 2 when type operation is built-in function into "0" | | "1" "2"
let a: StrNum = 0;
// Yes, the equation works
let b: Str_Num = 0;
Copy the code
Type of internal string operation
To facilitate string manipulation, TypeScript includes a set of types available for string manipulation. These types are built into the compiler to improve performance and cannot be found in files included in.d.stypescript.
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
type ShoutyGreeting = "HELLO, WORLD"
Copy the code
Starting with TypeScript 4.1, these built-in functions operate directly with JavaScript string runtime functions and are invisible to the user.
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
Please note that version requirements are above 4.1. It is recommended to use the VS Code editor and install JavaScript and TypeScript Nightly plug-ins
Conditions in the
A condition type is just a ternary expression: SomeType extends OtherType? TrueType: FalseType, which provides a logical branch processing capability for type inference, is a type expression that provides if… The else. The extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends
Conditional types are specified in typescript version 2.8. Check out the documentation for more details.
// Return the base type
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T0 = TypeName<string>; // "string"
/ / car
interface Car {
wheel: 4;
}
/ / house
interface House {
bed: true;
}
// The input type parameter T must satisfy the requirement of the car
// If it has both car and house properties, it is a car, otherwise it is an ordinary car
type CarType<T extends Car> = T extends Car & House ? "motorhome" : "car";
Copy the code
Distribution condition type
What happens to conditional types when the input type is union or cross?
- Union types: Distributed conditions automatically convert distributed union types, one constraint at a time, into type parameters, and then use the resulting type
|
merge - Cross type: it is a type and can be typed directly instead of being handled separately
/ / return type: "string" | "function"
// First: string Type TypeName(string) to get string
Type TypeName(() => void) to get function
/ / the result: a string, the function USES ` | ` link get joint type "string" | "function"
type T1 = TypeName<string | (() = > void) >;// Return type: function,
// The input type argument () => number satisfies the T extends Function constraint
type T2 = TypeName<string[] & (() = > number) >;Copy the code
Inference of type variables
In conditional types, declaring a type variable through the infer keyword delays inference. It can be understood from two aspects:
Type variable
: is a placeholder in a type structure that represents theThe specified location
On the various types of general referenceinference
: is a delayed process that does not infer the type of a type variable until the type parameter is input
/** * defines a Unpacked type Unpacked * U is a type variable that acts as a type placeholder in different positions to infer */
type Unpacked<T> = T extends (infer U)[]
? U
: T extends(... args:any[]) => infer U
? U
: T extends Promise<infer U>
? U
: T;
/** * Promise
[] If T extends (infer U)[] * the type variable U is the type placeholder * of the elements in the array (infer U), it can be inferred that U refers to Promise
*/
type TP = Unpacked<Promise<string> > [];// Promise<string>
Copy the code
The position of a type variable
According to the position of the type variable, it can be divided into covariant and contravariant. Wikipedia defines it as follows: “Covariance and contravariance are terms used in computer science to describe whether or not multiple types with parent/child relationships exist in complex forms constructed through type constructors.”
In a programming language’s type system, a type rule or type constructor is:
- Covariant, if it maintains subtype gender order relation ≦ variant. The order is: subtype ≦ base type.
- Contravariant, if it reverses the subtype order relation.
- Invariant if neither of the above applies.
type ReturnType<T> = T extends(... args:any[]) => infer R ? R : any;
/ / covariant
type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
type T10 = Foo<{ a: string; b: string} >.// string
type T11 = Foo<{ a: string; b: number} >.// string | number
/** ** contravariant * as a function parameter type, to restrict the type range more tightly, can */
type Bar<T> = T extends { a: (x: infer U) = > void; b: (x: infer U) = > void}? U :never;
type T20 = Bar<{ a: (x: string) = > void; b: (x: string) = > void} >.// string
type T21 = Bar<{ a: (x: string) = > void; b: (x: number) = > void} >.// string & number
/** * When inferring a type with multiple call signatures (such as function overload types), overloading */ cannot be resolved against the argument type list with the last signature
declare function foo(x: string) :number;
declare function foo(x: number) :string;
declare function foo(x: string | number) :string | number;
type T30 = ReturnType<typeof foo>; // string | number
Copy the code
Covariant and contravariant are both terms, the former referring to a type that can be derived to a greater (more specific) degree than the originally specified derived type, and the latter to a type that can be derived to a lesser (less specific) degree than the originally specified derived type.
Utility Types
Now that you have the ability to operate on types, it’s the original driver of type derivation. When we do type conversions to express complex types, we often use some other type to operate on. Typescript provides some built-in utility types that you can use directly.
Built-in type, you can command + click to view the definition, for more intuitive, easy to understand, the direct definition is copied out
Partial<Type>
Set all properties of Type to optional types
/ / define
type Partial<T> = {
// Set each property to an optional type
[P inkeyof T]? : T[P]; };type Item = {
name: string;
status: number;
};
// When modifying a data item, you can provide only a few of the attributes
function setItem(input: Partial<Item>) {
// do something
}
setItem({ name: "hello world" });
Copy the code
Required<Type>
Required: Sets all attributes of Type to the Type that must be provided
/ / define
type Required<T> = {
[P inkeyof T]-? : T[P]; };type Item = {
id: number; status? :number;
};
// When updating the status, the value of status must be provided
function setStatus(input: Required<Item>) {
// do something
}
// The status field is not provided
setStatus({ id: 0 });
Copy the code
Readonly<Type>
Readonly: Sets all properties of Type to read-only
/ / define
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Item = {
id: number;
name: string; status? :number;
};
{readonly id: number; readonly name: string; readonly status? : number; }
type LockItem = Readonly<Item>;
Copy the code
Record<Keys,Type>
Construct an object Type with an attribute key of Type Keys and an attribute value of Type Type. Can be used to map attributes of one type to another type
/ / define
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type Item = {
id: number;
name: string; status? :number;
};
type Keys = "a" | "b" | "c";
{a: Item; b: Item; c: Item; }
type ItemMap = Record<Keys, Item>;
// Essentially a mapping of one type of attribute to another type, a mapping of key-value pairs
type ItemList = Record<number, Item>;
const list: ItemList = [{ id: 1.name: "hello world" }];
Copy the code
Pick<Type, Keys>
Construct a Type by selecting a set of attributes Keys (a string literal or a union of string literals) in the set of attributes of Type Type (keyof Type).
- Keys must be in keyof Type, otherwise it is removed
- Keys that meet the above criteria are indexed to get the original type
/ / define
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Item = {
a: number;
b: string; c? :number;
};
type Keys = "a" | "b" | "d";
{a: number; b: string; }
// because a and B are Item attributes, d is not
type SubItem = Pick<Item, Keys>;
Copy the code
Omit<Type, Keys>
Remove Keys (string literals or union of string literals) from the set of Type Type attributes (keyof Type), and use the rest to construct a Type.
- Remove Keys from keyof Type and use the rest as Keys of the new Type
- The remaining keys that meet the above criteria are accessed by index to obtain the original type
/ / define
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Item = {
a: number;
b: string; c? :number;
};
type Keys = "a" | "b";
{c? : number; }
type SubItem = Omit<Item, Keys>;
Copy the code
Pick and Omit operations use keyof and index access, which are great tools for manipulating key-value pair types
Exclude<Type, ExcludedUnion>
From Type Type, a new Type is generated by excluding the specified ExcludedUnion.
- Type is an associative Type, otherwise exclusion is out of the question
- ExcludedUnion is also, literally, required to be a union type, but it is not required to be a union type, but a union type with only one type
- The keyword used for filtering is
extends
Satisfied with the
In essence, it is a filtering operation. The filtering condition is whether the Type in the ExcludedUnion can be assigned to Type. If so, it is excluded.
/ / define
type Extract<T, U> = T extends U ? T : never;
type Keys = "a" | "b" | "c" | "d";
/ / result: "c" | "d"
type ResKeys = Exclude<Keys, "a" | "b">;
Copy the code
Extract<Type, Union>
From the Type Type, extract the Union Type that can satisfy the bead specification to generate a new Type.
- Type is an associative Type, otherwise extraction would be impossible
- Union, literally, is also required to be a Union type, but this is not required, it is possible to be a type, but a Union type with only one type
- The key words used are
extends
Satisfied with the
In essence, it is a filtering operation, and the filtering condition is that the extracted type should satisfy the Union and be extracted as part of the new class.
/ / define
type Extract<T, U> = T extends U ? T : never;
type Keys = "a" | "b" | "c" | "d";
/ / operation results: "a" | "b"
type ComKeys = Extract<Keys, "a" | "b">;
Copy the code
NonNullable<Type>
Construct a new Type from Type Type, excluding null and undefined
/ / define
type NonNullable<T> = T extends null | undefined ? never : T;
// Result: string[]
type T1 = NonNullable<string[] | null | undefined>;
Copy the code
Typescript also provides type manipulation tools for parameter types, return value types, this types, and constructor types around function type operations.
Parameters<Type>
Construct a tuple type based on the type used in the parameter of a function type. Since javacript’s function argument is an array inside, it makes sense to return a meta-ancestor type.
/ / define
type Parameters<T extends(... args:any) = >any> = T extends(... args: infer P) =>any ? P : never;
const fun1 = (a: { name: string }, price: number) = > {
return a.name + print;
};
[a: {name: string;}, price: number]
type A = Parameters<typeof fun1>;
// The first type of the primitive type {name: string; }
type B = A[0];
Parameters returns a meta ancestor and only supports numeric index access
// a: { name: string; } is a description of a type, not an object
type C = A["a"];
Copy the code
ReturnType<Type>
Construct a type from the return value type of a function, which is the type to extract the return value of the function
/ / define
type ReturnType<T extends(... args:any) = >any> = T extends(... args:any) => infer R ? R : any;
const f1 = (): string[] = > {return [];
};
type RType = ReturnType<typeof f1>;
Copy the code
ConstructorParameters<Type>
Returns the argument type meta-ancestor of the constructor
/ / define
type ConstructorParameters<T extends abstract new(... args:any) = >any> = T extends abstract new(... args: infer P) =>any ? P : never;
interface Duck {
new (a: string.b: number) : {name: string };
}
type ConTor = ConstructorParameters<Duck>;
// Type error because F1 does not explicitly declare the new signature
type ProtoType = ConstructorParameters<typeof F1>;
Copy the code
InstanceType
Get the return type of the constructor, which is too official: return the type of the instance.
/ / define
type InstanceType<T extends abstract new(... args:any) = >any> = T extends abstract new(... args:any) => infer R ? R : any;
class Cat {}
function Dog() {}
type CatType = InstanceType<typeof Cat>;
let cat: CatType = new Cat();
// Type error: no new (... Args: any): any Signature
// Why is this so? The Dog constructor, which can be called as a normal argument
// class Cat cannot be called, only new is allowed
type DogType = InstanceType<typeof Dog>;
// Type error: attribute prototype is not present in Cat instance
// new Cat() returns an instance
let cat1: typeof Cat = new Cat();
// The type was derived successfully
// InstanceType is a typeof class["prototype"] syntax
let cat2: typeof Cat["prototype"] = new Cat();
Copy the code
ThisParameterType<Type>
Return the type of the function this because this, if present, must be placed first in the argument and can be easily extracted by infer.
type ThisParameterType<T> = T extends (this: infer U, ... args:any[]) = >any ? U : unknown;
class Duck {
name = "hello world";
}
function fun(this: typeof Duck) {
console.log(this.name);
}
const duck = new Duck();
// Specify that this refers to duck when called
fun.call(duck);
Copy the code
OmitThisParameter<Type>
Ignore the function’s this parameter type.
- Case 1: Return Type without declaring this parameter Type
- Case 2: If the type of this parameter is declared in type, the function ignores this and returns the remaining parameter types as the new function type. The return value type of the function remains unchanged.
type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends(... args: infer A) => infer R ?(. args: A) = > R : T;
function a(num: number) :string {
return `${num}`;
}
function b(this: Window, str: string) {}
(num: number) => string
type A = OmitThisParameter<typeof a>;
// Result: (STR: string) => void
type B = OmitThisParameter<typeof b>;
// bind this and return the new function
let bFun: B = b.bind(window);
Copy the code
ThisType<Type>
The ThisType type utility does not return the converted type; it serves as a marker for the this context type. This can only be used if the — noImplicitThis option is turned on.
/** * is defined as an empty type ** /
interface ThisType<T> { }
typeObjectDescriptor<D, M> = { data? : D;// The type of this in methodsk is D & Mmethods? : M & ThisType<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;
}
let obj = makeObject({
data: { x: 0.y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx; // Strongly typed this
this.y += dy; // Strongly typed this,}}}); obj.x =10;
obj.y = 20;
obj.moveBy(5.5);
Copy the code
Intrinsic String Manipulation Types
TypeScript contains a set of types that can be used for string operations in the type system. You can find these in the template literal type document. See the template text type above
conclusion
Type operations are at the heart of typescipt’s expressive capabilities, such as keyof, extends, in, Typeof, conditional types, infer type variables, and this. These operations, like addition, subtraction, multiplication, and division, must be intuitive to use.
When learning a programming technology, close to the business, try to write, there will be a different discovery, interested can have a look.
Typecript is a complete system, with declaration files and project configurations to learn more about. If there are any mistakes or omissions in this article, please correct them. Thank you.