This article mainly refers to the TYPEScript website’s TS guide, followed by a summary of how I felt about brushing type-challenge. This is not a translated version of the official guide.
Two words: If you don’t know the basics of interfaces, types, and generics, there’s no need to go on. If the type of gymnastics you do easy above all have no pressure can also not go down.
If anything is wrong or unclear, please let me know in the comments.
Agree first
The reason why many people find TypeScript difficult is that TS sometimes looks like JS, and sometimes it doesn’t. At the beginning, many people do not regard TS as a new language to learn, but simply regard TS as the extension of JS to learn. After actual learning, they find that there are many new knowledge points that they have not touched at all, and subconsciously think TS is difficult.
We need to learn TS as a new language, and Microsoft does not admit that TS is a superset of JS. Therefore, in the following study, I try to use JS to give you the analogy of TS, really not good when you need to remember more to learn this language, just like you learn JS at the beginning.
1. The generic
What is a generic
First, let’s write an identity function that returns whatever argument you pass in. Instead of using generics, we could write:
// This can only be done if arg is number
function identity(arg: number) :number {
return arg;
}
// Use any instead
function identity(arg: any) :any {
return arg;
}
Copy the code
While using the any type does the basic function of identity, because it causes the function to accept any type of arG type, we actually lose information about what the type is when the function returns. If we pass in a number, the only information we get is that any type can be returned, so it’s also not perfect.
Use generics to do this:
function identity<Type> (arg: Type) :Type {
return arg;
}
Copy the code
This version of the identity function conforms to the beginning of the input of any argument after the return argument, namely the identity function.
is used to capture the Type of the input parameter. General generic type is named T/U/V/P and so on. In the actual project, if many people develop the generic type can be named more specifically, such as Input, UISchema and so on.
Generic functions/interfaces
When the following generic function declarations are defined:
const myIdentity: <Type>(arg: Type) = > Type;
Copy the code
Define the form of the myIdentity function. Only the following functions can be assigned to myIdentity:
function identity<T> (arg:T) :T {
return arg
}
Copy the code
Generics can also be used with interfaces:
// The interface is written like this
interface GenericIdentityFn {
arg: number;
}
// Generic interface
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
let myIdentity: GenericIdentityFn<number> = identity;
Copy the code
As you can see, the above example takes generic parameters as parameters for the entire interface. This lets us see what type the generics of myIdentity end up being (e.g. GenericIdentityFn
and not just GenericIdentityFn). This makes type parameters much more readable to others reading the interface.
2. Type operator keyof + extends
Keyof chapter in TS Handbook
JS returns the value of a key in obj, usually like this:
function getOneObjValue (obj, key) {
return obj[key]
}
Copy the code
If the key entered is not obj, the function will return undefined, which may cause errors in subsequent code. General JS processing is to write if else in the function to filter out the edge case, cumulative function may have a variety of if else nested with each other, especially disgusting.
In TS, we need to add a type to the function:
function getOneObjValue (obj: T, key: keyof T) :T[keyof T] {
return obj[key]
}
Copy the code
In this case, keyof T represents the type of template literal formed by the key names of T, and the value range of key is locked as the key in T. TS reports an error if you enter a key that does not belong to T. The return type of the function is T[keyof T] (the concept is called index access), which is similar to the JS object value using obj[key]. Keyof does not need to write a for loop to retrieve it, similar to object.keys() in JS.
Here’s an example:
// Define an interface
interface Person {
"name": string."age": number
}
type p1 = keyof Person; // "name" | "age"
type p2 = Person["name"]; //p2 is of type string
type p3 = Person[keyof Person];/ / p3 type string | number
Copy the code
Interspersed with the following to see what the template literal type is:
// Template literals have the same syntax as JS template strings
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// The type of AllLocaleIDs is displayed as:
type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
Copy the code
There is room for improvement in the getOneObjValue function to implement generics. Keyof is repeated twice in the function, and we can replace it with a generic type variable. Here’s what we’ll end up writing:
function getOneObjValue (obj: T, key: keyof T) :T[keyof T] {
return obj[key]
}
/ / the final version
function getOneObjValue<T extends object.U extends keyof T> (obj: T, key: U) :T[U] {
return obj[key]
}
Copy the code
First we use T/P/U/V to represent a generic variable. What does extends mean? In TS, extends in A type like this is not the same extends as subClass extends Class in JS, where A extends B can simply be understood to mean that the former is constrained by the latter or that the former is A subset of the latter. In T extends Object, object is one of the basic types, that is, an object, so it can be understood that the generic T is a subset of objects or that T is restricted to an object type. Similarly, U can be understood.
Now, assuming that instead of taking a value from obj, the function should be modified as follows:
function getOneObjValue<T extends object.U extends keyof T> (obj: T, key: U) :T[U] {
return obj[key]
}
/ / array
function getObjValueArr
<T extends object.U extends keyof T> (obj: T, keys: U[]) :T[U] []{
return keys.map((key) = > obj[key]);
}
Copy the code
Keys: U[] and T[U][]. The basics here are:
// Define the number type
let a:number = 1;
// Define the array type
let arr1:number[] = [1.2.3];
Copy the code
Type operator Typeof + Index access type
The type operator typeof
Typeof in TS is similar to typeof in JS.
// JS
console.log(typeof "Hello world"); / / output string
//TS
let s = "hello";
let n: typeof s; // indicates that n is of type string
Copy the code
Typeof takes the typeof the following variable (value!), but note that value refers to an untyped variable (value!). , and get a Type (Type!). Complete the type declaration. Because in TS type programming, you can’t put non-type variables (values!) In the type operator, so we need typeof for the conversion.
Here’s an example:
First, we write a function type, which applies a type constraint to a variable’s function definition:
// Predicate is a function definition
type Predicate = (x: unknown) = > boolean;
// ReturnType is the built-in function type of TS. It can be used directly. It is used to take the type of the return value of the function type
type K = ReturnType<Predicate>; // the type of K is Boolean
Copy the code
Then, let’s write another function and use ReturnType in the function name:
function f() {
return { x: 10.y: 3 };
}
// An error is reported because the type operator can only operate on types
type P = ReturnType<f>;
// Use typeof to fetch the type
type P = ReturnType<typeof f>; Type P = {x:number; y:number }
Copy the code
Typeof can also be used in the following cases:
const MyArray = [
{ name: "Alice".age: 15 },
{ name: "Bob".age: 23 },
{ name: "Eve".age: 38},];Type Person = {name: string; age: number; }
type Person = typeof MyArray[number];
// Type Age = number
type Age = typeof MyArray[number] ["age"];
// You can also take the number type
type Age2 = Person["age"];
Copy the code
Person[“age”] is a new concept called Index Access Type. In TS, we can use an index access type to find a specific attribute on another type:
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // The Age type is number
Copy the code
This is similar to taking the value obj[key] in JS, and taking the type definition of the interface property in TS. MyArray[number] takes the values of all elements in the array (📢 is a value, not a type), but here we need the types of all elements, just add typeof in front of it. Typeof MyArray[number][“age”] typeof MyArray[number][“age”]
In JS, when you hear indexes, you think of arrays. For example, Type[number] and typeof Type[‘ a property ‘] are common in TS arrays. For example, Type[number] and typeof Type[‘ a property ‘] are common in TS arrays.
4. Condition type
What is the condition type
Conditions in the
There’s if else in JS, not in TS. The TS conditional type is somewhat similar to the JS conditional operator.. ? . :.
SomeType extends OtherType ? TrueType : FalseType;
Copy the code
This can often be used for function overloading. If you don’t understand what function overloading is, look at the following example:
interface IdLabel {
id: number;
}
interface NameLabel {
name: string;
}
function createLabel(id: number) :IdLabel;
function createLabel(name: string) :NameLabel;
function createLabel(nameOrId: string | number) :IdLabel | NameLabel;
function createLabel(nameOrId: string | number) :IdLabel | NameLabel {
throw "unimplemented";
}
Copy the code
The createLabel function needs to produce different output depending on the input parameter. This is called function overloading. Without conditional types, we would have to repeatedly define the type of function needed in each scenario. Mindless repetition is a bad thing for programmers.
The createLabel function actually has three overloaded scenarios: ID, name, and nameOrId. For the first two types, the return value is determined. For the nameOrId type, if the input is uncertain, the function returns IdLabel or NameLabel. So we use the condition type to define a type to describe the function’s return value definition:
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
Copy the code
Return IdLabel if T is constrained to number, namellabel otherwise.
We can then use this condition type to simplify overloading into a single function so that overloading is no longer necessary:
function createLabel<T extends number | string> (idOrName: T) :NameOrId<T> {
throw "unimplemented";
}
let a = createLabel("typescript"); // a:NameLabel
let b = createLabel(2.8); // b:IdLabel
let c = createLabel(Math.random() ? "hello" : 42); // c: NameLabel | IdLabel
Copy the code
Conditional Type Constraints
The conditional type constraint allows for a more specific inferred type. Just as narrowing (also known as narrowing, later) for a type with type guard (more on this later) provides a more specific role for the type, the code to be executed for the part of the conditional type that is true further constrains generics. Hence the concept of conditional-type constraints.
Look at the following example:
type MessageOf<T> = T["message"]; Type '"message"' cannot be used to index Type 'T'
Copy the code
The reason for the error is that there is no message on the generic T until the user enters it. Therefore, we need to constrain T so that the code does not report an error.
Normally, without the condition type constraint, you would write:
type MessageOf<T extends { message: unknown }> = T["message"];
// Test it
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>; / / EmailMessageContents: string
Copy the code
However, if we want MessageOf to be of any type other than {message: unknown} and to default to never in cases where the message attribute is not available, we can do this by removing the constraint and introducing conditional types.
T[“message”] is bound by {message: unknown}; T[“message”] is bound by {message: unknown}; T[“message”] defaults to never. This is not a pure type of condition:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
//type EmailMessageContents = string
type EmailMessageContents = MessageOf<Email>;
//type DogMessageContents = never
type DogMessageContents = MessageOf<Dog>;
Copy the code
The revised MessageOf is more intuitive than the previous one.
As an exercise after learning about type constraints, write a Flatten type for an array. This type extracts the types of the elements in the array, and returns the current type if not the array type. When it comes to yes and no, we need to think about whether the conditional constraint is available:
type Flatten<T> = T extends any[]? T[number] : T;
Any [] and Array
are two ways to represent arrays
type Flatten<T> = T extends Array<any>? T[number] : T;
/ / test
type Str = Flatten<string[] >;// Str:string
type Num = Flatten<number>;// Num:number
Copy the code
T[number] is the index access type that we talked about earlier. For array generics, T[number] means to fetch all the elements of the array, because the index of the array is a number, and the number type contains all the numbers, thus fetching all the elements.
Infer the keyword
Flatten has room to improve. But why not start by infer after we learn how to use the keyword ‘infer’? Conditional types give us a way to derive type results from true branch code using the Infer keyword. Infer is used only with conditional types.
First look at the syntax:
infer T;
Copy the code
Infer T must be a type variable to be inferred, which means to infer T together.
The Flatten function can also be written like this:
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
Copy the code
In this case, Item represents the type to be inferred. In this case, we don’t know what type Item is. This is better than the last version where we don’t need T[number] to figure out how to get the type of every element in an array, and we introduce a new Item generic variable that directly represents the type of the element to be passed in (inferred) in the future.
Infer Many people may be a little confused by the key word infer, but here’s a little more:
First, Infer only serves conditional types.
Second, the representation of a conditional type is to narrow down the range of output types based on the generic conditions of the input, that is, the input type is indeterminate. For uncertain items, we can either infer by ourselves: similar to the Flatten function that does not know the type of each element in the input array, we can fetch each element type by T[number]; Or we can introduce new generic variables (like Item) to represent unknown element types, but we must infer that Item is an unknown Item. In javascript, a constant must be preceded by const to indicate that the variable behind it is a constant.
And finally, infer is used only with conditional types; Infer is followed by the generics to be inferred.
To verify the results, let’s implement a GetReturnType: Use Infer to implement the built-in generic ReternType, which extracts return types from function types (that’s what we’ll infer about generics).
type GetReturnType<T> = T extends(... args:never[]) => infer R ? R : never;
type Str = GetReturnType<(x: string) = > string>; //Str:string
type Bools = GetReturnType<(a: boolean, b: boolean) = > boolean[] >;//Bools:boolean[]
Copy the code
R is introduced to indicate the generics to be inferred, and infer is preceded if the input generic T is indeed constrained to function types (… Args: never[]) => infer R returns type R, otherwise return never.
Distributed condition type
It’s a fancy name, but it’s just one of the things you need to be aware of when you’re writing conditional types, or you can call it a conditional type feature.
When a conditional type is applied to a generic type, it becomes a distributed type when a union type is passed to the generic.
Write an example based on the above statement:
// The following Type converts the passed generic Type to an array Type
type ToArray<Type> = Type extends any ? Type[] : never;
// Pass a union type
type StrArrOrNumArr = ToArray<string | number>; / / the StrArrOrNumArr: string [] | number []
Copy the code
To Type is joint Type string | number, and then assign to ToArray < string > | ToArray < number >, finally it is concluded that the string [] | number []. This is the distributed condition type.
If you don’t know condition type distribution characteristics, may think that the result is (string | number) []. To really get this result, you need to make some simple changes:
type ToArrayNonDist<Type> = [Type] extends [any]? Type[] :never;
Copy the code
After wrapping Type, there is no distributed situation.
5. Mapping type
Mapping type
Map types are built on the syntax of index signatures, which are used to declare undeclared attribute types.
So before we map the type, let’s talk about the index code.
When you want to declare a type or interface, you only know the key and value types. You can use the index code to represent this:
type OnlyBoolsAndHorses = {
[key: string] :boolean | Horse;
};
/ / equivalent to the
type OnlyBoolsAndHorses1 = Record<string.boolean | Horse>;
const conforms: OnlyBoolsAndHorses = {
del: true.rodney: false};Copy the code
The type OnlyBoolsAndHorses represents an unknown key name and quantity, except that the key name is of type string and the key value is of type Boolean or Horse. This is called an index code. At this time can also use TS built-in function Type Record
to express, are the same meaning.
Let’s talk about mapping types. Suppose we now need to write an interface B that has the same properties as interface A, then we need to map the property types of A to interface B using mapping types. Of course, you can also be stupid and manually copy a copy of the changes, but this is still not a good solution.
Look at the following example:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
Copy the code
In the above example, OptionsFlags takes all the attributes in Type Type and changes their values to Boolean values. {keyof Type}} {keyof Type} {keyof Type}} {keyof Type} {keyof Type}} in… By analogy, take each of the Type attributes in turn and constrain it to a Boolean Type. Since we don’t know what the future attributes of OptionsFlags are, we can only infer the definition of the constraint attributes from the generic Type, so we use the index code [… :… To express the final definition of the OptionsFlags type.
It can be seen that each syntax in TS is very concise, and the bottom layer has already helped us to achieve most functions. It is not as complicated as JS language (for example, the loop must have the for or while keyword), but the disadvantage of such encapsulation is that it is difficult to understand at first sight. This is quite different from writing JS, and it takes practice to get used to it.
Continuing, let’s do what we said at the beginning of this section to map the attribute type of A to interface B:
interface A {
"name": string;
"age": number;
}
type mappingAToB<T> = {
[K in keyof T]: T[K];
};
letB: mappingAToB < A >;// The definition of B is the same as that of interface A
Copy the code
We can write Partial, which makes all properties optional: Partial
type Partial<T> = {
[K inkeyof T]? : T[k]; };Copy the code
Will the optional symbol be? Replace this with readonly, which makes all attributes read-only and gives you a built-in type readonly:
type Readonly<T> = {
readonly [K in keyof T]: T[k];
};
Copy the code
It’s called hot blacksmithing. – the symbol can be used in? And readonly to remove the following attributes. Match it -? You get another built-in type: Required. -? Remove optional, then all attributes are Required. This is not Required!
type Required<T> = {
[K inkeyof T]-? : T[k]; };Copy the code
Brother Jimei meng, see here is not feel super simple!! There is no problem, your TS is slowly progressing to advanced.
6. Type Guards
The front will be conditional type, buried a type guard pit. This word sounds lofty and confusing at first glance, mainly because of the literal translation of Type Guards. In fact, AFTER my own understanding, I think it might be better to translate it into type protection. What type guarding really means is that in TS you can narrow a wide range of types down to a small range of types, and these tricks in TS will make sure that the program doesn’t go wrong. They are called type guarding.
Some TS keywords, such as is, Typeof, in, and instanceof, can act as type guards. Typeof (a) === ‘string’ (a) == ‘string’ (b);
In TS, typeof is a fine type guard because it fixes a bug in JS:
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") { / / null object
for (const s of strs) { Object is possibly 'null'
console.log(s); }}}Copy the code
Null in JS is also object. If you don’t write TS, this code is very likely to be problematic later on. So in TypeScript, checking against typeof returns is type protection (guarding).
Here’s another simple example:
For union types, union types are sometimes separated by defining type, which in effect Narrows them down to a single type
type Transportation = { type:'car'; car:number } | {type:'bus'; bus:number}
/ / narrow
function tran(arg:Transportation) {
// Type guard
if(arg.type === 'car') {{type:'car'; car:number }
} else{... }}Copy the code
Arg. Type here is also an action of type protection.
Is the keyword
Next is to do type protection.
type a: number | string;
Copy the code
If you want to narrow a to string, use JS logic and use Typeof for if and else. Let’s try this first:
// Define an isString function to check if it is a string, and return true if it is
const isString = (arg: unknown): boolean= > typeof arg === "string";
/ / use
function useIt(numOrStr: number | string) {
if (isString(numOrStr)) {
console.log(numOrStr.length); / / error: number | on does not contain the string length attribute}}Copy the code
Typeof arg === “string”; typeof arg === “string”;
const isString = (arg: unknown): arg is string= >typeof arg === "string";
Copy the code
If typeof arg === “string”, isString must be a string. UseIt does not return the same guard as typeof arg === “string”. This reencapsulation is also known as custom type guarding.
Type guarding can be used in practice to verify external unknown data, such as data passed from back end to front end. Since TS is precompiled and verified before running, unknown data at run time cannot be guaranteed. At this time, a custom type guard needs to be written to ensure the normal operation of the program.
In the keyword
The in keyword was used earlier in keyof. JS also has the in operator, which determines whether an object contains an attribute:
var myObject = {
foo: 1};'foo' in myObject; // true
Copy the code
With this in mind, TS uses it as a way to narrow down potential types.
Here’s an example:
type Fish = { swim: () = > void };
type Bird = { fly: () = > void };
function move(animal: Fish | Bird) {
if ("swim" in animal) { // Use in to exclude other types on the union type
return animal.swim();
}
return animal.fly();
}
Copy the code
One thing to note is that optional attributes pass in limits, which in cannot guard.
type Fish = { swim: () = > void };
type Bird = { fly: () = > void };
typeHuman = { swim? :() = > void; fly? :() = > void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
animal; // (parameter) animal: Fish | Human
} else {
animal; // (parameter) animal: Bird | Human}}Copy the code
7. Multiple ways to narrow types
Narrow type
The TS Handbook has a special chapter on how to narrow types in different situations, which consists of 11 sections. The type guard mentioned above covers several of them: Typeof, in, IS, and separable union types, and the rest of the section, where the idea is mostly identical to that of JS.
Use the equality operator
Use the = =,! = = = =,! == narrow type:
1. Use congruence ===
function example(x: string | number, y: string | boolean) {
if (x === y) {
x.toUpperCase(); // x:string
y.toLowerCase(); // y:string
} else {
console.log(x);
console.log(y); }}Copy the code
2. Use == :
The == symbol is not strictly equal in JS and can be used to narrow in TS. It has a nice effect on null and undefined, use! =null not only removes null from the type, but also removes undefined. The same is true for undefined! =undefined also removes null.
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// both 'null' and 'undefined' are removed.
if(container.value ! =null) {
console.log(container.value); //(property) Container.value: number
// It is safe to calculate 'container.value'container.value *= factor; }}Copy the code
instanceof
This is exactly the same as JS.
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString()); // (parameter) x: Date
} else {
console.log(x.toUpperCase());// (parameter) x: string}}Copy the code
The assignment
let x:number | string;
x = 1;
let a = x; // a:number
x = 'hello';
let b = x; // b:string
x = true; //Type 'boolean' is not assignable to type 'string | number'
let c = x; //c:number|string
Copy the code
When assigned to a variable with a wide type range, TS Narrows the type range of the left variable appropriately based on the value on the right.
But note that every assignment TS is (declare) according to a statement from the original type to be narrowed, will not be because of the type of assignment to change the original statement, so in the above example of c type number | string.
never
The never type can be assigned to every type, but no type can be assigned to never (except itself). Use this feature to perform exhaustive type checking on switch statements.
First look at the JS notation:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
// ---cut---
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2; }}Copy the code
In JS, if there is no default to handle, default will not be written, or the writing logic will not be able to handle the new type. For example, my colleague of Shape needed to add an interface Triangle later, but he didn’t know that the type of Shape was used in the switch you wrote before. In this case, he should rely on default. In TS, you need to use the never type as a bottom pocket.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
interface Triangle {
kind: "triangle";
sideLength: number;
}
// ---cut---
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape; // Triangle cannot assign to type never
return_exhaustiveCheck; }}Copy the code
Const _exhaustiveCheck: never = shape; . Shape in the first case of narrow for Square | Triangle, the second case narrowed to Triangle, so there will be a Triangle cannot be assigned to never type of error.
If there is no Triangle type, two cases narrow the type to no type, and shape becomes never, which can be assigned to never, so no error is reported.
To review again: the never type can be assigned to every type, but no type can be assigned to never (except itself). At first glance, the never type seems redundant, and this example helps us understand the use of the never type.
If, in a project, you need to ensure that every type in a large range of types is checked (also known as infinite checking), you need to use this in combination with the never feature.
conclusion
In fact, TS is not difficult, mainly a lot of professional terms are unfamiliar, but as long as the concept of JS and better understand. Some type operators of TS and JS have the same name but different concepts and usages.
My own more suitable way is to brush type gymnastics, first think about how to use JS to achieve, and then corresponding to TS to achieve, so that the early for the entry of the students more friendly.