weak and strong typing
Programming languages can be divided into strongly typed languages and weakly typed languages according to whether the data types are fixed.
weakly typed language
Weakly typed language: Variables, function parameters, and function return values have no type. A variable can receive any type of data, and a function can also return any type of data.
let n = 10;
n = "abc";
strongly typed language
Strongly typed language: Variables, function parameters, etc. all have fixed types. When declaring a variable, the type of the variable must be specified. A variable of a type can only receive data of this type.
int n = 10;
n = 10.0; //错误
n = "qqqq" //错误
Strongly typed languages: C/C#/Java, etc.
Weak type: Javascript/lua/python/php etc.
Pros and cons of weakly typed languages:
- Advantages: easy to use and flexible. No extensive type conversions are required.
- Disadvantages: Not rigorous enough to judge the type of a variable or parameter from the code, which is not conducive to the development of large-scale projects by multiple people.
Pros and cons of strongly typed languages:
- Advantages: rigorous logic, definite type, suitable for developing large-scale projects.
- Disadvantages: During the development process, you need to pay attention to the type of data at all times, and you need to manually convert data between different types when calculating.
TypeScript
TypeScript is called TS for short.
The relationship between TS and JS
- The relationship between TS and JS is actually the relationship between Less/Sass and CSS
- Just like Less/Sass extends CSS, TS also extends JS
- Just like Less/Sass will eventually be converted to CSS, the TS code we write will eventually be converted to JS
Introduction to TypeScript
TS is a superset (extension set) of JS. It implements strong typing on the basis of JS grammar, that is to say, TS is strongly typed JS, TS extends JS, and has things that JS does not have. TS is just a syntax standard, it has no execution environment, and TS code will eventually be converted into JS code and run in the JS execution environment.
- TS is a superset of JS.
- TS implements strong typing on the basis of JS syntax.
- TS extends JS, introduces the concept of types to JS, and adds many new features.
- TS code needs to be compiled into JS by a compiler, and then executed by a JS parser.
- TS is fully compatible with JS, in other words, any JS code can be directly used as TS.
- Compared with JS, TS has static typing, stricter syntax, and more powerful functions;
- TS can complete the code inspection before the code is executed, reducing the chance of runtime exceptions;
- TS code can be compiled into any version of JS code, which can effectively solve the compatibility problem of different JS operating environments;
- For the same function, the amount of code in TS is larger than that in JS, but because the code structure of TS is clearer and the variable types are clearer, TS is far better than JS in later code maintenance.
Why do you need TypeScript?
- Simply put, because JavaScript is weakly typed, many errors are only discovered at runtime
- And TypeScript provides a set of static detection mechanisms that can help us find errors at compile time
TypeScript Features
- Support for the latest JavaScript features
- Support code static checking
- Supports features in backend languages such as C, C++, Java, Go, etc. (enums, generics, type conversions, namespaces, declaration files, classes, interfaces, etc.)
official website
https://www.tslang.cn/index.html
TypeScript development environment construction
Install TS
Download the TS compiler, which can convert TS files to JS files.
npm install -g typescript
Compile TS
Manual compilation: Use the tsc command to convert a TS file into a JS file.
tsc index.ts
//如果报错ts-node : 无法将“ts-node”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
//可以试试 npx tsc index.ts 这种运行成功后如果需要使用Code Runner插件的话,需要将插件运行命令更换为npx开头,往下看有更换的步骤
Automatic compilation: When compiling a file, after using the -w command, the TS compiler will automatically monitor the change of the file and recompile the file when the file changes.
tsc index.ts -w
//如果不行的话可以试试 npx tsc index.ts -w
Run TS in vsCode
Run TS files in vsCode, or run JS files directly
-
VSCode installs the Code Runner plugin
-
Install the ts-node module globally: npm i ts-node -g
-
Use the right key
Run Code
or shortcut keyalt+ctrl+n
to run the TS file (remember here that the project path cannot contain Chinese characters, otherwise the path cannot be found, or an error will be reported directly)(If the output code has Chinese garbled characters, you can go to the Code Runner plug-in settings search
Run in Terminal
to select it)(If the path does not contain Chinese or an error is reported, just find
setting.json
an edit point in the plug-in settings, findcode-runner.executorMap
the option, findtypescript
the value and replace it withnpx ts-node
Save, so that it will run according to the specified command when you right-click to run)
type of data
type declaration
-
Type declaration is a very important feature of TS
-
The type of variables (parameters, formal parameters) in TS can be specified by type declaration
-
After specifying the type, when assigning a value to the variable, the TS compiler will automatically check whether the value conforms to the type declaration, assign the value if it matches, or report an error
-
In short, a type declaration sets a type for a variable so that it can only store values of a certain type
let 变量: 类型;
let 变量: 类型 = 值;
function fn(参数: 类型, 参数: 类型): 类型{
...
}
type
type | example | describe |
---|---|---|
number | 1, -33, 2.5 | any number |
string | 'Hihi",hi |
You can use double quotes, single quotes, template strings to represent strings |
boolean | true、false | boolean true or false |
null | null | null has its own type called null |
undefined | undefined | undefined has its own type called undefined |
object | {name:'Zhang San'} | any JS object |
array | [1,2,3] | Arbitrary JS array |
tuple | [4,5] | Tuple, TS new type, fixed-length array |
any | * | any type |
Literal | itself | The value of the restricted variable is the value of the literal |
void | Null value (undefined) | no value (or undefined) |
never | no value | cannot be any value |
unknown | * | type-safe any |
enum | enum{A, B} | Enumeration, a new type in TS |
basic type
let str: string = "张三";
let num: number = 18;
let bool: boolean = false;
let u: undefined = undefined;
let n: null = null;
let obj: object = {name:'张三'};
let big: bigint = 100n;
let sym: symbol = Symbol("foo");
Array
There are two ways to define the array type: use Array<类型>
or 类型[]
two ways
let names: Array<string> = ['tom', 'lily', 'lucy'];
let ages: number[] = [22, 23, 24];
Define an array of union types
// 表示定义了一个名称叫做arr的数组,
// 这个数组中将来既可以存储数值类型的数据, 也可以存储字符串类型的数据
let arr:(number | string)[];
arr = [1, 'b', 2, 'c'];
Define an array of specified object members:
// interface是接口
interface Arrobj{
name:string,
age:number
}
let arr:Arrobj[] = [{
name:'jimmy',age:22}];
Tuple (tuple)
Yuan ancestor definition
As we all know, arrays are generally composed of values of the same type, but sometimes we need to store values of different types in a single variable, then we can use tuples. There are no tuples in JavaScript, tuples are a type specific to TypeScript that work like arrays.
The most important feature of tuples is that they can be restricted 数组元素的个数和类型
, which is especially suitable for implementing multi-value returns.
Yuan ancestors are used to save data of fixed-length and fixed data types
let x: [string, number];
// 类型必须匹配且个数必须为2
x = ['hello', 10]; // OK
x = ['hello', 10, 10]; // Error
x = [10, 'hello']; // Error
Note that the tuple type can only represent an array with a known number and type of elements, the length has been specified, and an error will be prompted for out-of-bounds access. If there may be multiple types in an array, the number and type are uncertain, then directlyany[]
Destructuring assignment of tuple types
We can access the elements in the tuple by subscripting, which is not so convenient when there are many elements in the tuple. In fact, tuples also support destructuring assignment:
let employee: [number, string] = [1, "Semlinker"];
let [id, username] = employee;
console.log(`id: ${
id}`);
console.log(`username: ${
username}`);
After the above code runs successfully, the console will output the following message:
id: 1
username: Semlinker
It should be noted here that when destructuring and assigning, if the number of destructured array elements cannot exceed the number of elements in the tuple, otherwise errors will also occur, such as:
let employee: [number, string] = [1, "Semlinker"];
let [id, username, age] = employee;
In the above code, we added an age variable, but at this point the TypeScript compiler will prompt the following error message:
Tuple type '[number, string]' of length '2' has no element at index '2'.
Obviously the length [number, string]
of is 2
that there is no element 2
at .
optional elements of tuple type
With the function signature type, when defining the tuple type, we can also declare the optional elements of the tuple type through ?
the symbol . The specific example is as follows:
let optionalTuple: [string, boolean?];
optionalTuple = ["Semlinker", true];
console.log(`optionalTuple : ${
optionalTuple}`);
optionalTuple = ["Kakuqo"];
console.log(`optionalTuple : ${
optionalTuple}`);
In the above code, we define a variable optionalTuple
named , which is required to contain a required string attribute and an optional Boolean attribute. After the code runs normally, the console will output the following:
optionalTuple : Semlinker,true
optionalTuple : Kakuqo
So what does declaring optional tuple elements do in practice? Here we give an example. In a three-dimensional coordinate axis, a coordinate point can (x, y, z)
be expressed in the form of . For a two-dimensional coordinate axis, a coordinate point can (x, y)
be expressed in the form of . For a one-dimensional coordinate axis, as long as (x)
Use the form to express it. For this situation, in TypeScript, the characteristics of optional elements of tuple type can be used to define a coordinate point of tuple type. The specific implementation is as follows:
type Point = [number, number?, number?];
const x: Point = [10]; // 一维坐标点
const xy: Point = [10, 20]; // 二维坐标点
const xyz: Point = [10, 20, 10]; // 三维坐标点
console.log(x.length); // 1
console.log(xy.length); // 2
console.log(xyz.length); // 3
the remaining elements of the tuple type
The last element in the tuple type can be the remaining elements, in the form of ...X
, here X
is the array type. The remaining elements representing the tuple type are open to zero or more additional elements. For example, to represent a tuple type [number, ...string[]]
with one number
element and any number of elements of type. string
For better understanding, let's take a concrete example:
type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"];
console.log(restTuple[0]);
console.log(restTuple[1]);
read-only tuple type
TypeScript 3.4 also introduces new support for read-only tuples. We can prefix any tuple type with readonly
the keyword to make it a read-only tuple. Specific examples are as follows:
const point: readonly [number, number] = [10, 20];
After using readonly
the keyword to modify the tuple type, any operation that attempts to modify the elements in the tuple will throw an exception:
// Cannot assign to '0' because it is a read-only property.
point[0] = 1;
// Property 'push' does not exist on type 'readonly [number, number]'.
point.push(0);
// Property 'pop' does not exist on type 'readonly [number, number]'.
point.pop();
// Property 'splice' does not exist on type 'readonly [number, number]'.
point.splice(1, 1);
null and undefined
By default null
and undefined
are subtypes of all types. That means you can assign null
and undefined
to other types.
// null和undefined赋值给string
let str:string = "666";
str = null
str= undefined
// null和undefined赋值给number
let num:number = 666;
num = null
num= undefined
// null和undefined赋值给object
let obj:object ={};
obj = null
obj= undefined
// null和undefined赋值给Symbol
let sym: symbol = Symbol("foo");
sym = null
sym= undefined
// null和undefined赋值给boolean
let isDone: boolean = false;
isDone = null
isDone= undefined
// null和undefined赋值给bigint
let big: bigint = 100n;
big = null
big= undefined
If you specify in tsconfig.json "strictNullChecks":true
, null
and undefined
can only be assigned to void
and their respective types.
number and bigint
Although number
and bigint
both represent numbers, the two types are not compatible.
let big: bigint = 100n;
let num: number = 6;
big = num;
num = big;
will throw a type incompatible ts(2322) error.
void
void
Indicates that there is no type, and it is an equal relationship with other types, and cannot be directly assigned:
let a: void;
let b: number = a; // Error
You can only assign null
and undefined
(when strictNullChecks
not specified as true) to it. It is not very useful to declare void
a variable of a type, and we generally only declare it when the function does not return a value.
It's worth noting that the method will get no return value undefined
, but we need to define it as void
a type, not undefined
a type. Otherwise an error will be reported:
function fun(): undefined {
console.log("this is TypeScript");
};
fun(); // Error
never
never
Types represent the types of values that never exist.
There are two cases where the value will never exist:
- If a function throws an exception during execution, then this function will never have a return value (because throwing an exception will directly interrupt the program running, which makes the program run until the step of returning the value, that is, it has an unreachable end point, and it will never return. does not exist returned);
- The code that executes an infinite loop ( infinite loop ) in the function makes the program never run to the step where the function returns the value, and there is never a return.
// 异常
function err(msg: string): never {
// OK
throw new Error(msg);
}
// 死循环
function loopForever(): never {
// OK
while (true) {
};
}
never
Type, like null
and undefined
, is also a subtype of any type, and can also be assigned to any type.
But no type is never
a subtype of or is assignable to never
a type (other than never
itself), any
not even tonever
let ne: never;
let nev: never;
let an: any;
ne = 123; // Error
ne = nev; // OK
ne = an; // Error
ne = (() => {
throw new Error("异常"); })(); // OK
ne = (() => {
while(true) {
} })(); // OK
In TypeScript, you can use the characteristics of the never type to implement comprehensive checks. The specific examples are as follows:
type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {
if (typeof foo === "string") {
// 这里 foo 被收窄为 string 类型
} else if (typeof foo === "number") {
// 这里 foo 被收窄为 number 类型
} else {
// foo 在这里是 never
const check: never = foo;
}
}
Note that in the else branch, we assign foo, narrowed to never, to an explicitly declared never variable. If all the logic is correct, then it should be able to compile here. But suppose one day your colleague modifies the type of Foo:
type Foo = string | number | boolean;
However, he forgot to modify the control flow in controlFlowAnalysisWithNever
the method . At this time, the foo type of the else branch will be narrowed to boolean
the type, so that it cannot be assigned to the never type, and a compilation error will occur. In this way, we can ensure that controlFlowAnalysisWithNever
the method always exhausts all possible types of Foo. Through this example, we can draw a conclusion: use never to avoid the occurrence of new joint types without corresponding implementations, and the purpose is to write absolutely type-safe code.
any
In TypeScript, any type can be classified as any type. This makes the any type the top-level type of the type system.
If it is a normal type, changing the type during assignment is not allowed:
let a: string = 'seven';
a = 7;
// TS2322: Type 'number' is not assignable to type 'string'.
But if it is any
of type , it is allowed to be assigned to any type.
let a: any = 666;
a = "Semlinker";
a = false;
a = 66
a = undefined
a = null
a = []
a = {
}
Accessing any property on any is allowed, as is calling any method.
let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');
If a variable is declared without specifying its type, it will be recognized as any value type :
let something;
something = 'seven';
something = 7;
something.setName('Tom');
Equivalent to
let something: any;
something = 'seven';
something = 7;
something.setName('Tom');
In many scenarios, this is too loose. Using any
types , it's easy to write code that is correctly typed but has problems at runtime. If we use any
the type , we can't use a lot of protection mechanisms provided by TypeScript. Remember, any 是魔鬼!
try not to use any.
To solve the problems any
caused by , TypeScript 3.0 introduces unknown
the type.
unknown
unknown
As any
with , all types are assignable to unknown
:
let notSure: unknown = 4;
notSure = "maybe a string instead"; // OK
notSure = false; // OK
unknown
The biggest difference with any
is:
- Values of any type can be assigned to
any
, andany
values of any type can be assigned to any type. - Values of any type can be assigned to
unknown
, butunknown
can only be assigned tounknown
andany
types, andunknown
cannot be assigned to other types
let notSure: unknown = 4;
let uncertain: any = notSure; // OK
let notSure: any = 4;
let uncertain: unknown = notSure; // OK
let notSure: unknown = 4;
let uncertain: number = notSure; // Error
unknown
You can't do anything with a type without narrowing it:
function getDog() {
return '123'
}
const dog: unknown = {
hello: getDog};
dog.hello(); // Error
This mechanism is very preventive and safer, which requires us to narrow down the type. We can use methods such as typeof
, 类型断言
and so on to narrow down the unknown range:
function getDogName() {
let x: unknown;
return x;
};
const dogName = getDogName();
// 直接使用
const upName = dogName.toLowerCase(); // Error
// typeof
if (typeof dogName === 'string') {
const upName = dogName.toLowerCase(); // OK
}
// 类型断言
const upName = (dogName as string).toLowerCase(); // OK
Number、String、Boolean、Symbol
First of all, let's review the Number, String, Boolean, and Symbol types that are easily confused with the primitive types number, string, boolean, and symbol when we first learn TypeScript. The latter are the corresponding primitive types, let's call 包装对象
them for the object type.
From the perspective of type compatibility, the primitive type is compatible with the corresponding object type, and the object type is incompatible with the corresponding primitive type.
Let's look at a specific example:
let num: number;
let Num: Number;
Num = num; // ok
num = Num; // ts(2322)报错
In line 3 of the example, we can assign number to type Number, but assigning Number to number in line 4 will prompt ts(2322) error.
Therefore, we need to keep in mind not to use object type to annotate the type of value, because it doesn't make any sense.
object、Object 和 {}
In addition, object (lowercase initial letter, hereinafter referred to as "small object"), Object (capital initial letter, hereinafter referred to as "big Object") and {} (hereinafter referred to as "empty object")
The small object represents all non-primitive types, that is to say, we cannot assign primitive types such as number, string, boolean, and symbol to object. In strict mode, null
and undefined
types are also not assignable to object.
The following types are considered primitive types in JavaScript:
string
,boolean
,number
,bigint
,symbol
,null
andundefined
.
Let's look at a concrete example:
let lowerCaseObject: object;
lowerCaseObject = 1; // ts(2322)
lowerCaseObject = 'a'; // ts(2322)
lowerCaseObject = true; // ts(2322)
lowerCaseObject = null; // ts(2322)
lowerCaseObject = undefined; // ts(2322)
lowerCaseObject = {
}; // ok
Lines 2~6 in the example will prompt ts(2322) error, but after we assign an empty object to object in line 7, it can pass the static type detection.
Big Object represents all types with toString and hasOwnProperty methods, so all primitive types and non-primitive types can be assigned to Object. Likewise, in strict mode, null and undefined types cannot be assigned to Object.
Let's also look at a specific example below:
let upperCaseObject: Object;
upperCaseObject = 1; // ok
upperCaseObject = 'a'; // ok
upperCaseObject = true; // ok
upperCaseObject = null; // ts(2322)
upperCaseObject = undefined; // ts(2322)
upperCaseObject = {
}; // ok
In the example, lines 2 to 4 and line 7 can pass static type detection, while lines 5 to 6 will prompt ts(2322) error.
As can be seen from the above example, the large Object contains primitive types, and the small object only contains non-primitive types, so the large Object seems to be the parent type of the small object. In fact, big Object is not only the parent type of small object, but also the subtype of small object.
Below we still use a specific example to illustrate.
type isLowerCaseObjectExtendsUpperCaseObject = object extends Object ? true : false; // true
type isUpperCaseObjectExtendsLowerCaseObject = Object extends object ? true : false; // true
upperCaseObject = lowerCaseObject; // ok
lowerCaseObject = upperCaseObject; // ok
In the example, the returned types of lines 1 and 2 are both true, and upperCaseObject and lowerCaseObject in lines 3 and 4 can be assigned to each other.
Note: Although the official document says that small objects can be used instead of large Objects, we still need to understand that large Objects are not completely equivalent to small objects.
The {} empty object type is the same as the big Object, which also represents a collection of primitive types and non-primitive types, and in strict mode, null and undefined cannot be assigned to {}, as shown in the following example:
let ObjectLiteral: {
};
ObjectLiteral = 1; // ok
ObjectLiteral = 'a'; // ok
ObjectLiteral = true; // ok
ObjectLiteral = null; // ts(2322)
ObjectLiteral = undefined; // ts(2322)
ObjectLiteral = {
}; // ok
type isLiteralCaseObjectExtendsUpperCaseObject = {
} extends Object ? true : false; // true
type isUpperCaseObjectExtendsLiteralCaseObject = Object extends {
} ? true : false; // true
upperCaseObject = ObjectLiteral;
ObjectLiteral = upperCaseObject;
In the example, the return types of lines 8 and 9 are both true, ObjectLiteral and upperCaseObject in lines 10 and 11 can be assigned to each other, and the assignment operations in lines 2~4 and 7 conform to static type detection ; and the 5th and 6th lines will prompt ts(2322) error.
In summary: {} and large Object are broader types (least specific) than small objects. {} and large Object can replace each other to represent primitive types (except null and undefined) and non-primitive types; while small objects represents a non-primitive type.
function
function declaration
-
When declaring a function, you need to specify the type of the parameter
-
A function with no return value can add void to indicate that the return value of the function is empty
-
The actual parameter passed when calling the function should also be consistent with the formal parameter type
function fn(x: number, y: number): void {
console.log(x + y);
}
function sum(x: number, y: number): number {
return x + y;
}
sum(10, 20);
function expression
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
Define function types with interfaces
interface SearchFunc{
(source: string, subString: string): boolean;
}
When using the function expression interface to define a function, the type restriction on the left side of the equal sign can ensure that the number of parameters, parameter types, and return value types remain unchanged when assigning a function name in the future.
optional parameters
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
Note: Required parameters are not allowed after optional parameters
parameter default value
function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
remaining parameters
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
function overloading
Since JavaScript is a dynamic language, we usually use different types of parameters to call the same function, and the function will return different types of call results according to different parameters:
function add(x, y) {
return x + y;
}
add(1, 2); // 3
add("1", "2"); //"12"
Since TypeScript is a superset of JavaScript, the above code can be used directly in TypeScript, but when the TypeScript compiler noImplicitAny
opens configuration item, the above code will prompt the following error message:
Parameter 'x' implicitly has an 'any' type.
Parameter 'y' implicitly has an 'any' type.
This information tells us that the parameters x and y implicitly have any
type . To solve this problem, we can set a type for the parameter. Since we want add
the function to support both string and number types, we can define a string | number
union type and give it an alias:
type Combinable = string | number;
After defining the Combinable union type, let's update add
the function :
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
add
After explicitly setting the type for the parameter of the function , the previous error message disappeared. So is add
the function , let's actually test it:
const result = add('Semlinker', ' Kakuqo');
result.split(' ');
In the above code, we use 'Semlinker'
and ' Kakuqo'
the two strings as parameters to call the add function, and save the result of the call to a variable result
named . At this time, we take it for granted that the type of the variable result is string at this time, so We can call split
the method . But at this time, the TypeScript compiler has the following error message again:
Property 'split' does not exist on type 'number'.
number
Obviously there is no split
property on objects of type . The problem is coming again, how to solve it? At this point we can take advantage of the function overloading feature provided by TypeScript.
Function overloading or method overloading is the ability to create multiple methods with the same name and different number or types of parameters. To solve the problems encountered above, the method is to provide multiple function type definitions for the same function for function overloading, and the compiler will process the function call according to this list.
type Types = number | string
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Types, b: Types) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
const result = add('Semlinker', ' Kakuqo');
result.split(' ');
In the above code, we provide multiple function type definitions for the add function to realize function overloading. Afterwards, the nasty error message disappears again, because the result variable is now of string
type type .
type inference
- TS will automatically infer the type of the variable according to the context
- When the declaration and assignment of variables are carried out at the same time, the TS compiler will automatically determine the type of the variable
- If the variable is declared and assigned at the same time, the type declaration can be omitted
{
let str: string = 'this is string';
let num: number = 1;
let bool: boolean = true;
}
{
const str: string = 'this is string';
const num: number = 1;
const bool: boolean = true;
}
Looking at the above example, you may have been muttering: variables that define basic types need to write type annotations. TypeScript is too troublesome, right? In the example, when using let to define a variable, we just write the type annotation, after all, the value may be changed. However, when using const
constants you need to write type annotations, which is really troublesome.
In fact, TypeScript has long considered such a simple and obvious problem.
In many cases, TypeScript will automatically infer the type of the variable according to the context, without us needing to write type annotations. Therefore, the above example can be simplified to something like this:
{
let str = 'this is string'; // 等价
let num = 1; // 等价
let bool = true; // 等价
}
{
const str = 'this is string'; // 不等价
const num = 1; // 不等价
const bool = true; // 不等价
}
We call TypeScript's ability to infer types based on assignment expressions 类型推断
.
In TypeScript, variables with initialized values, function parameters with default values, and function return types can all be inferred from context. For example, we can infer the type returned by the function according to the return statement, as shown in the following code:
{
/** 根据参数的类型,推断出返回值的类型也是 number */
function add1(a: number, b: number) {
return a + b;
}
const x1= add1(1, 1); // 推断出 x1 的类型也是 number
/** 推断参数 b 的类型是数字或者 undefined,返回值的类型也是数字 */
function add2(a: number, b = 1) {
return a + b;
}
const x2 = add2(1);
const x3 = add2(1, '1'); // ts(2345) Argument of type "1" is not assignable to parameter of type 'number | undefined
}
If there is no assignment at the time of definition, regardless of whether there is an assignment afterwards, it will be inferred as any
a type and will not be type checked at all:
let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
type assertion
Sometimes you'll run into a situation where you know more details about a value than TypeScript does. Usually this happens when you clearly know that an entity has a more specific type than it already has.
This is the way to tell the compiler, "trust me, I know what I'm doing" through type assertions. Type assertions are like type conversions in other languages, but without special data checking and destructuring. It has no runtime impact, it only works during the compile phase.
TypeScript type detection cannot be absolutely intelligent, after all, programs cannot think like humans. Sometimes we run into situations where we know the actual types better than TypeScript does, like in the following example:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)
Among them, greaterThan2 must be a number (3 to be precise), because there are obviously members greater than 2 in arrayNumber, but static typing can't do anything to the logic at runtime.
From the perspective of TypeScript, the type of greaterThan2 may be either a number or undefined, so the above example prompts a ts(2322) error, and we cannot assign the type undefined to the type number at this time.
However, we can use a certain way - type assertion (similar to a type cast that only works at the type level) to tell TypeScript to do type checking in our way.
For example, we can use the as syntax to make type assertions, as shown in the following code:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;
grammar
// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
Although there is no difference between the above two methods, the angle bracket format will cause grammatical conflicts with JSX in react, so we recommend using the as syntax.
non-null assertion
In contexts where the type checker cannot determine the type, a new postfix expression operator !
can be used to assert that the operand is of type non-null and non-undefined. Specifically, x! will exclude null and undefined from the x range.
See the following examples for details:
let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)
type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
// Object is possibly 'undefined'.(2532)
// Cannot invoke an object which is possibly 'undefined'.(2722)
const num1 = numGenerator(); // Error
const num2 = numGenerator!(); //OK
}
sure assignment assertion
Allows you to place a !
sign to tell TypeScript that the property will be explicitly assigned. To better understand what it does, let's look at a concrete example:
let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
x = 10;
}
Obviously, the exception message means that the variable x is used before the assignment. To solve this problem, we can use the assertion to determine the assignment:
let x!: number;
initialize();
console.log(2 * x); // Ok
function initialize() {
x = 10;
}
With a let x!: number;
definite assignment assertion, the TypeScript compiler knows that the property will definitely be assigned a value.
literal type
In TypeScript, literals can represent not only values, but also types, so-called literal types.
Currently, TypeScript supports 3 literal types: string literal type, numeric literal type, and Boolean literal type. The corresponding string literals, numeric literals, and Boolean literals have the same literal type as their values. Specific examples are as follows:
{
let specifiedStr: 'this is string' = 'this is string';
let specifiedNum: 1 = 1;
let specifiedBoolean: true = true;
}
For example, 'this is string' (here means a string literal type) type is string type (exactly a subtype of string type), but the string type is not necessarily 'this is string' (here means a string literal type Quantity type) type, the following specific example:
{
let specifiedStr: 'this is string' = 'this is string';
let str: string = 'any string';
specifiedStr = str; // ts(2322) 类型 '"string"' 不能赋值给类型 'this is string'
str = specifiedStr; // ok
}
For example, we use "horse" as a metaphor for the string type, that is, "dark horse" refers to the type of 'this is string'. "Dark horse" must be "horse", but "horse" is not necessarily "dark horse", it may still be "white horse". "Grey Horse". Therefore, the 'this is string' literal type can assign a value to the string type, but the string type cannot assign a value to the 'this is string' literal type. This metaphor is also suitable for describing the relationship between other literals such as numbers and Booleans and their parent classes. .
string literal type
Generally speaking, we can use a string literal type as the variable type, as shown in the following code:
let hello: 'hello' = 'hello';
hello = 'hi'; // ts(2322) Type '"hi"' is not assignable to type '"hello"'
In fact, it is not very useful to define a single literal type. Its real application scenario is that multiple literal types can be combined into a joint type (explained later), which is used to describe a practical collection with clear members .
As shown in the following code, we use the literal union type to describe a clear set that can be 'up' or 'down', so that we can clearly know the required data structure.
type Direction = 'up' | 'down';
function move(dir: Direction) {
// ...
}
move('up'); // ok
move('right'); // ts(2345) Argument of type '"right"' is not assignable to parameter of type 'Direction'
By using the union type of literal type combination, we can restrict the parameters of the function to the specified literal type set, and then the compiler will check whether the parameter is a member of the specified literal type set.
Thus, using literal types (composed union types) allows function parameters to be restricted to more specific types than using string types. This not only improves the readability of the program, but also guarantees the parameter type of the function, which can be said to kill two birds with one stone.
Number Literal Types and Boolean Literal Types
The use of numeric literal types and Boolean literal types is similar to the use of string literal types. We can use the union type of literal combinations to limit the parameters of functions to more specific types, such as declaring a type Config as shown below :
interface Config {
size: 'small' | 'big';
isEnable: true | false;
margin: 0 | 2 | 4;
}
In the above code, we limit the size attribute to be the string literal type 'small' | 'big', and the isEnable attribute to be the Boolean literal type true | false (the Boolean literal only contains true and false, the combination of true | false and There is no difference if you use boolean directly), and the margin attribute is a numeric literal type 0 | 2 | 4.
let and const analysis
Let's first look at a const example, as shown in the following code:
{
const str = 'this is string'; // str: 'this is string'
const num = 1; // num: 1
const bool = true; // bool: true
}
In the above code, we define const as an unchangeable constant. In the case of default type annotations, TypeScript infers that its type is directly determined by the type of the assigned literal, which is also a reasonable design.
Next we look at the let example shown below:
{
let str = 'this is string'; // str: string
let num = 1; // num: number
let bool = true; // bool: boolean
}
In the above code, the type of the changeable variable of the default explicit type annotation is converted to the parent type of the assignment literal type, for example, the type of str is 'this is string' type (here represents a string literal type) The parent type is string, and the type of num is the parent type number of type 1.
This design conforms to programming expectations, which means that we can assign str and num any value (as long as the type is a variable of a subset of string and number):
str = 'any string';
num = 2;
bool = false;
We call this design of converting TypeScript’s literal quantum type to the parent type “literal widening”, which is the widening of the literal type. For example, the string literal type mentioned in the above example is converted to the string type. Below we Let me introduce it emphatically.
Type Widening
All variables defined by let or var, formal parameters of functions, and non-read-only properties of objects, if they meet the conditions of specifying initial values and not explicitly adding type annotations, then their inferred types are the specified initial value literals The type after type widening is literal type widening.
Let's understand the widening of literal types through the example of string literals:
let str = 'this is string'; // 类型是 string
let strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string;
const specifiedStr = 'this is string'; // 类型是 'this is string'
let str2 = specifiedStr; // 类型是 'string'
let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string;
Because lines 1 and 2 satisfy the conditions of let and formal parameters without explicitly declaring type annotations, the types of variables and formal parameters are expanded to string (the formal parameter type is exactly string | undefined).
Because the constant in line 3 cannot be changed, the type is not widened, so the type of specifiedStr is 'this is string' literal type.
Lines 4~5, because the assigned value specifiedStr is a literal type, and there is no explicit type annotation, so the types of variables and formal parameters are also widened. In fact, such a design meets the actual programming demands. Let's imagine that if the type of str2 is inferred as 'this is string', it will be immutable, because assigning any other value of type string will prompt a type error.
Based on the conditions of literal type widening, we can control the type widening behavior by adding explicit type annotations through the code shown below.
{
const specifiedStr: 'this is string' = 'this is string'; // 类型是 '"this is string"'
let str2 = specifiedStr; // 即便使用 let 定义,类型是 'this is string'
}
In fact, in addition to literal type widening, TypeScript also has a design similar to "Type Widening" (type widening) for certain specific types of values. Let's take a look at it in detail below.
For example, to widen the types of null and undefined, if the variables defined by let and var satisfy the undeclared type annotation and are assigned the value of null or undefined, the type of these variables is inferred to be any:
{
let x = null; // 类型拓宽成 any
let y = undefined; // 类型拓宽成 any
/** -----分界线------- */
const z = null; // 类型是 null
/** -----分界线------- */
let anyFun = (param = null) => param; // 形参类型是 null
let z2 = z; // 类型是 null
let x2 = x; // 类型是 null
let y2 = y; // 类型是 undefined
}
Note: In strict mode, null and undefined in some older versions (2.0) are not widened to "any".
In order to understand type widening more conveniently, let's take an example and analyze it more deeply.
Suppose you are writing a vector library, you first define a Vector3 interface, and then define the getComponent function to get the value of the specified axis:
interface Vector3 {
x: number;
y: number;
z: number;
}
function getComponent(vector: Vector3, axis: "x" | "y" | "z") {
return vector[axis];
}
However, when you try to use the getComponent function, TypeScript will prompt the following error message:
let x = "x";
let vec = {
x: 10, y: 20, z: 30 };
// 类型“string”的参数不能赋给类型“"x" | "y" | "z"”的参数。
getComponent(vec, x); // Error
Why does the above error occur? We know from TypeScript's error message that the type of the variable x is inferred to be of type string, and the getComponent function expects a more specific type for its second parameter. This was widened in practice, so it resulted in a bug.
This process is complicated because there are many possible types for any given value. For example:
const arr = ['x', 1];
What should be the type of the above arr variable? Here are some possibilities:
- (‘x’ | 1)[]
- [‘x’, 1]
- [string, number]
- readonly [string, number]
- (string | number)[]
- readonly (string|number)[]
- [any, any]
- any[]
Without more context, TypeScript has no way of knowing which type is "correct", it has to guess your intent. As smart as TypeScript is, it can't read your mind. It's not guaranteed to be 100% correct, as we just saw with inadvertent errors.
In the following example, the type of the variable x is inferred to be string because TypeScript allows such code:
let x = 'semlinker';
x = 'kakuqo';
x = 'lolo';
For JavaScript, the following code is also legal:
let x = 'x';
x = /x|y|z/;
x = ['x', 'y', 'z'];
TypeScript tries to strike a balance between specificity and flexibility when inferring the type of x to be string. The general rule is that the type of a variable should not change after declaration, so string makes more sense than string|RegExp or string|string[] or any string.
TypeScript provides a few ways to control the widening process. One of the ways is to use const
. If a variable is declared const instead of let, its type will be narrower. In fact, using const can help us fix the error in the previous example:
const x = "x"; // type is "x"
let vec = {
x: 10, y: 20, z: 30 };
getComponent(vec, x); // OK
Because x cannot be reassigned, TypeScript can infer the narrower type and there will be no errors in subsequent assignments. Because the string literal "x" is assignable to "x"|"y"|"z", the code passes the type checker.
However, const 并不是万灵药。对于对象和数组,仍然会存在问题
.
The following code is fine in JavaScript:
const obj = {
x: 1,
};
obj.x = 6;
obj.x = '6';
obj.y = 8;
obj.name = 'semlinker';
Whereas in TypeScript, for obj's type, it can be {readonly x:1}
of type , or the more general {x:number}
type of . Of course, it may also be of type {[key: string]: number}
or object. For objects, TypeScript's widening algorithm treats its internal properties as assigning them to variables declared with the let keyword, and then infers the types of its properties. So obj is of type {x:number}
. This allows you to assign obj.x to other variables of type number instead of variable of type string, and it also prevents you from adding other attributes.
So the last three lines of the statement will throw an error:
const obj = {
x: 1,
};
obj.x = 6; // OK
// Type '"6"' is not assignable to type 'number'.
obj.x = '6'; // Error
// Property 'y' does not exist on type '{ x: number; }'.
obj.y = 8; // Error
// Property 'name' does not exist on type '{ x: number; }'.
obj.name = 'semlinker'; // Error
TypeScript tries to strike a balance between specificity and flexibility. It needs to infer a type specific enough to catch the error, but not the type of the error. It infers the type of a property from its initial value, and of course there are several ways to override TypeScript's default behavior. One is to provide explicit type annotations:
// Type is { x: 1 | 3 | 5; }
const obj: {
x: 1 | 3 | 5 } = {
x: 1
};
Another approach is to use const assertions. Not to be confused with let and const, which introduce symbols in the value space. This is a pure type-level construct. Let's look at the different inferred types for the following variables:
// Type is { x: number; y: number; }
const obj1 = {
x: 1,
y: 2
};
// Type is { x: 1; y: number; }
const obj2 = {
x: 1 as const,
y: 2,
};
// Type is { readonly x: 1; readonly y: 2; }
const obj3 = {
x: 1,
y: 2
} as const;
When you use a const assertion after a value, TypeScript will infer the narrowest type for it, no widening. For real constants, this is usually what you want. Of course you can also use const assertions on arrays:
// Type is number[]
const arr1 = [1, 2, 3];
// Type is readonly [1, 2, 3]
const arr2 = [1, 2, 3] as const;
Since there is type widening, there will naturally be type narrowing. Let's briefly introduce Type Narrowing.
Type Narrowing
In TypeScript, we can narrow the type of variables from a relatively broad set to a relatively small and clear set through certain operations, which is "Type Narrowing".
For example, we can use type guards (described later) to narrow the type of function parameters from any to specific types, as follows:
{
let func = (anything: any) => {
if (typeof anything === 'string') {
return anything; // 类型是 string
} else if (typeof anything === 'number') {
return anything; // 类型是 number
}
return null;
};
}
In VS Code, the prompt type of the anything variable from hover to line 4 is string, and the prompt type to line 6 is number.
Similarly, we can use type guards to narrow down union types to explicit subtypes, as shown in the following example:
{
let func = (anything: string | number) => {
if (typeof anything === 'string') {
return anything; // 类型是 string
} else {
return anything; // 类型是 number
}
};
}
Of course, we can also converge the joint type to a more specific type through literal type equivalent judgment (===) or other control flow statements (including but not limited to if, ternary operator, switch branch), as shown in the following code Show:
{
type Goods = 'pen' | 'pencil' |'ruler';
const getPenCost = (item: 'pen') => 2;
const getPencilCost = (item: 'pencil') => 4;
const getRulerCost = (item: 'ruler') => 6;
const getCost = (item: Goods) => {
if (item === 'pen') {
return getPenCost(item); // item => 'pen'
} else if (item === 'pencil') {
return getPencilCost(item); // item => 'pencil'
} else {
return getRulerCost(item); // item => 'ruler'
}
}
}
In the above getCost function, the accepted parameter type is the joint type of literal type, and the function contains three process branches of if
the statement , and the parameters of the function called by each process branch are specific and independent literal types.
Then why can a variable item whose type consists of multiple literals be passed to a function that only accepts a single specific literal type getPenCost、getPencilCost、getRulerCost
? This is because in each flow branch, the compiler knows what type of item is in the flow branch. For example, if item === branch of 'pencil', the type of item is shrunk to "pencil".
In fact, if we remove the middle process branch from the above example, the compiler can also infer the converged type, as shown in the following code:
const getCost = (item: Goods) => {
if (item === 'pen') {
item; // item => 'pen'
} else {
item; // => 'pencil' | 'ruler'
}
}
In general TypeScript
is very good at identifying types by condition, but be careful when dealing with some special values - it may contain things you don't want! For example, the following method of excluding null from a union type is an error:
const el = document.getElementById("foo"); // Type is HTMLElement | null
if (typeof el === "object") {
el; // Type is HTMLElement | null
}
Since typeof null
in the result is "object", you're not actually excluding null
values from this check. Besides that, raw values of falsy create similar problems:
function foo(x?: number | string | null) {
if (!x) {
x; // Type is string | number | null | undefined\
}
}
Since the empty string and 0 are falsy values, the type of x in the branch may be string or number. Another common way to help the type checker narrow down types is to put an explicit "label" on them:
interface UploadEvent {
type: "upload";
filename: string;
contents: string;
}
interface DownloadEvent {
type: "download";
filename: string;
}
type AppEvent = UploadEvent | DownloadEvent;
function handleEvent(e: AppEvent) {
switch (e.type) {
case "download":
e; // Type is DownloadEvent
break;
case "upload":
e; // Type is UploadEvent
break;
}
}
This pattern is also known as "tag union" or "discriminated union", and it has a wide range of applications in TypeScript.
union type
The union type indicates that the value can be one of multiple types, and each type |
is separated .
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven'; // OK
myFavoriteNumber = 7; // OK
Union types are null
often undefined
used with or :
const sayHello = (name: string | undefined) => {
/* ... */
};
For example, name
here the type of is string | undefined
means string
that undefined
the value of or can be passed to sayHello
the function.
sayHello("semlinker");
sayHello(undefined);
From this example, you can intuit that the union of type A and type B results in a type that accepts both A and B values. Additionally, for union types, you may encounter the following usages:
let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';
In the above example 1
, 2
or 'click'
is called literal type, which is used to constrain the value to be only one of several values.
type alias
type
: Type aliases are used to give a new name to a type, and the same type can be used multiple times with one name.- Type aliases are often used with union types.
type type alias syntax:
type Point = {
x: number;
y: number;
};
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({
x: 100, y: 100 });
Type aliases can be used to name any type, not just object types. Commonly used for union types.
type test = number; //基本类型
let num: test = 10;
type userOjb = {
name:string} // 对象
type getName = ()=>string // 函数
type data = [number,string] // 元组
type numOrFun = number | getName // 联合类型
Note: Type aliasing, as the name suggests, means we just give the type a new name, not create a new type.
cross type
Intersecting types is combining multiple types into one type. This allows us to superimpose existing types into one type, which contains all the required characteristics of the type, using the &
definition intersection type.
{
type Useless = string & number;
}
Obviously, if we only merge atomic types such as primitive types, literal types, and function types into cross types, it is useless, because any type cannot satisfy multiple atomic types at the same time, such as both string type and number type. Therefore, in the above code, the type of the type alias Useless is never.
The real use of the cross type is to merge multiple interface types into one type, so as to achieve the effect of equivalent interface inheritance, which is the so-called merged interface type, as shown in the following code:
type IntersectionType = {
id: number; name: string; } & {
age: number };
const mixed: IntersectionType = {
id: 1,
name: 'name',
age: 18
}
In the above example, we use the intersection type to make the IntersectionType have all the attributes of id, name, and age at the same time. Here we can try to understand the merged interface type as a union set.
think
Here, let's think about it divergently: what will happen if there are attributes with the same name in multiple interface types that are merged?
If the types of attributes with the same name are incompatible, for example, in the above example, the type of the name attribute of the two interface types with the same name is number and the other is string. After merging, the type of the name attribute is the cross type of the two atomic types of number and string, that is never, as shown in the following code:
type IntersectionTypeConfict = {
id: number; name: string; }
& {
age: number; name: number; };
const mixedConflict: IntersectionTypeConfict = {
id: 1,
name: 2, // ts(2322) 错误,'number' 类型不能赋给 'never' 类型
age: 2
};
At this point, if we assign any type of name attribute value to mixedConflict, it will prompt a type error. And if we don't set the name attribute, it will prompt an error that the required name attribute is missing. In this case, it means that the IntersectionTypeConfict type intersected in the above code is a useless type.
If the types of attributes with the same name are compatible, for example, one is number and the other is a subtype of number or a literal type of number, the type of the name attribute after merging is the subtype of the two.
The type of the name attribute in the example shown below is the number literal type 2, so we cannot assign any value other than 2 to the name attribute.
type IntersectionTypeConfict = {
id: number; name: 2; }
& {
age: number; name: number; };
let mixedConflict: IntersectionTypeConfict = {
id: 1,
name: 2, // ok
age: 2
};
mixedConflict = {
id: 1,
name: 22, // '22' 类型不能赋给 '2' 类型
age: 2
};
So what happens if the attribute with the same name is a non-primitive data type. Let's look at a concrete example:
interface A {
x:{
d:true},
}
interface B {
x:{
e:string},
}
interface C {
x:{
f:number},
}
type ABC = A & B & C
let abc:ABC = {
x:{
d:true,
e:'',
f:666
}
}
After the above code runs successfully, the following output will be output:
{
x: {
d: true, e: '', f: 666 } }
From the above results, we can see that when mixing multiple types, if there are the same members and the member types are non-basic data types, then the merge can be successful.
object oriented
object-oriented concepts
Object-oriented is a very important idea in programming. It is understood by many students as a difficult and esoteric problem, but it is not. Object-oriented is very simple. In short, all operations in the program need to be done through objects.
- for example:
- To operate the browser, use the window object
- To operate a web page, use the document object
- The operation console needs to use the console object
All operations must go through objects, which is the so-called object-oriented, so what exactly is an object? Let’s first talk about what a program is. The essence of a computer program is the abstraction of real things. The antonym of abstraction is concrete. For example, a photo is an abstraction of a specific person, and a car model is an abstraction of a specific car. A program is also an abstraction of things. In a program, we can represent a person, a dog, a gun, a bullet, and all other things. When a thing arrives in the program, it becomes an object.
All objects in the program are divided into two parts: data and functions. Taking people as an example, their name, gender, age, height, weight, etc. belong to data, and people can talk, walk, eat, and sleep, which belong to human functions. . Data are called properties in objects, and functions are called methods. So in short, everything is an object in a program.
class
If you want to be object-oriented and manipulate objects, you must first have objects, so the next question is how to create objects. To create an object, you must first define a class. The so-called class can be understood as the model of the object, and the object of the specified type can be created according to the class in the program.
For example: Human objects can be created through the Person class, dog objects can be created through the Dog class, and car objects can be created through the Car class. Different classes can be used to create different objects.
- Define class:
class 类名 {
属性名: 类型;
constructor(参数: 类型){
this.属性名 = 参数;
}
方法名(){
....
}
}
- Example:
class Person{
name: string;
age: number;
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
sayHello(){
console.log(`大家好,我是${
this.name}`);
}
}
- Use class:
const p = new Person('张三', 18);
p.sayHello();
Object Oriented Features
encapsulation
-
An object is essentially a container for attributes and methods, and its main function is to store attributes and methods, which is called encapsulation
-
By default, the properties of an object can be modified arbitrarily. In order to ensure data security, the permissions of properties can be set in TS
Read-only attribute (readonly):
- If you add a readonly when declaring a property, the property becomes a read-only property and cannot be modified
Properties in TS have three modifiers:
- public: public property (default value), can be accessed and modified anywhere: current class, subclasses and objects
- protected: protected, can be accessed and modified in the current class and subclasses
- private : Private property, which can only be accessed or modified in the current class
public
class Person{
public name: string; // 写或什么都不写都是public
public age: number;
constructor(name: string, age: number){
this.name = name; // 可以在类中修改
this.age = age;
}
sayHello(){
console.log(`大家好,我是${
this.name}`);
}
}
class Employee extends Person{
constructor(name: string, age: number){
super(name, age);
this.name = name; //子类中可以修改
}
}
const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 可以通过对象修改
protected
class Person{
protected name: string;
protected age: number;
constructor(name: string, age: number){
this.name = name; // 可以修改
this.age = age;
}
sayHello(){
console.log(`大家好,我是${
this.name}`);
}
}
class Employee extends Person{
constructor(name: string, age: number){
super(name, age);
this.name = name; //子类中可以修改
}
}
const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 不能修改
private
class Person{
private name: string;
private age: number;
constructor(name: string, age: number){
this.name = name; // 可以修改
this.age = age;
}
sayHello(){
console.log(`大家好,我是${
this.name}`);
}
}
class Employee extends Person{
constructor(name: string, age: number){
super(name, age);
this.name = name; //子类中不能修改
}
}
const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 不能修改
attribute accessor
-
For some properties that do not want to be modified arbitrarily, they can be set to private
-
Setting it directly to private will result in no longer being able to modify its properties through the object
-
We can define a set of methods to read and set properties in a class. This kind of property that reads or sets properties is called a property accessor
-
The method of reading a property is called a setter method, and the method of setting a property is called a getter method
class Person{
private _name: string;
constructor(name: string){
this._name = name;
}
get name(){
return this._name;
}
set name(name: string){
this._name = name;
}
}
const p1 = new Person('孙悟空');
console.log(p1.name); // 通过getter读取name属性
p1.name = '猪八戒'; // 通过setter修改name属性
static property
- Static properties (methods), also known as class properties. Using static properties does not need to create an instance, it can be used directly through the class
- Static properties (methods) start with static
- Without the static modifier, it is the properties and instance methods of the instance (object), these are the properties and methods of the instance
- With the static modifier, there are static properties and static methods, which are the properties and methods of the class
class Tools{
static PI = 3.1415926;
static sum(num1: number, num2: number){
return num1 + num2
}
}
console.log(Tools.PI);
console.log(Tools.sum(123, 456));
-
this
- In non-static methods (instance methods) this refers to the instance of the class
- In a static method this refers to the class
-
Access to methods and properties
- Instance attributes and instance methods can be accessed in non-static methods (instance methods), accessed through this
- Static properties and static methods can also be used in non-static methods (instance methods), called by class names
- In a static method, you cannot use instance attributes, and you cannot call instance methods
- In a static method, you can call static properties and static methods
-
instance properties and static properties
- Instance attributes and instance methods are called by the instance (object)
- Static properties and static methods can only be called by class name
inherit
extends
-
Inheritance is yet another feature of object-oriented
-
Properties and methods from other classes can be introduced into the current class through inheritance
class Animal{
name: string;
constructor(name: string){
this.name = name;
}
}
class Dog extends Animal{
age: number;
constructor(name: string, age: number) {
super(name);
this.age = age;
}
bark(){
console.log(`${
this.name}在汪汪叫!`);
}
}
const dog = new Dog('旺财', 4);
dog.bark();
Extensions to a class can be done without modifying the class through inheritance.
Classes inherit properties and methods from base classes. Here, Dog
is a derived class that derives from Animal
the base class via extends
the keyword . Derived classes are often called subclasses , and base classes are often called superclasses .
The derived class contains a constructor, which must be called super()
, which executes the base class constructor. Moreover, we must call before accessing properties this
of . This is an important rule enforced by TypeScript.super()
rewrite
- When inheritance occurs, if the method in the subclass replaces the method with the same name in the parent class, this is called method rewriting
class Animal{
name: string;
age: number;
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
run(){
console.log(`父类中的run方法!`);
}
}
class Dog extends Animal{
bark(){
console.log(`${
this.name}在汪汪叫!`);
}
run(){
super.run();
console.log(`子类中的run方法,会重写父类中的run方法!`);
}
}
const dog = new Dog('旺财', 4);
dog.bark();
- Super can be used in subclasses to complete references to parent classes
abstract class
- An abstract class is a class specially designed to be inherited by other classes, it can only be inherited by other classes and cannot be used to create instances
abstract class Animal{
abstract run(): void;
bark(){
console.log('动物在叫~');
}
}
class Dog extends Animals{
run(){
console.log('狗在跑~');
}
}
- A method beginning with abstract is called an abstract method. An abstract method has no method body and can only be defined in an abstract class. When inheriting an abstract class, the abstract method must be implemented.
interface
In TypeScript, we use interfaces (Interfaces) to define types of objects.
what is an interface
In object-oriented language, interface (Interfaces) is a very important concept. It is an abstraction of behavior, and how to act specifically needs to be implemented by classes (classes).
- interface interface: defined specification
- implements: Implementation: A class implements an interface, that is, implements the properties and methods defined by the interface
The interface in TypeScript is a very flexible concept. In addition to being used to [abstract a part of the behavior of the class], it is also often used to describe the "shape of the object (Shape)".
simple example
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: 'Tom',
age: 25
};
In the above example, we defined an interface Person
, and then defined a variable tom
whose type is Person
. In this way, we constrain the shape tom
of must Person
be consistent with the interface .
Interfaces are generally capitalized.
It is not allowed to define variables with fewer attributes than interfaces:
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: 'Tom'
};
// index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
// Property 'age' is missing in type '{ name: string; }'.
More attributes are also not allowed:
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
// index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
It can be seen that when assigning a value, the shape of the variable must be consistent with the shape of the interface .
optional | read-only property
interface Person {
readonly name: string;
age?: number;
}
Read-only properties are used to restrict the modification of its value only when the object is just created. In addition, TypeScript also provides ReadonlyArray<T>
the type , which Array<T>
is similar to except that all variable methods are removed, so it can ensure that the array cannot be modified after it is created.
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
any attribute
Sometimes we want an interface to allow other arbitrary attributes in addition to mandatory and optional attributes. At this time, we can use the form of index signature to meet the above requirements.
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
name: 'Tom',
gender: 'male'
};
It should be noted that once any attribute is defined, the type of both the definite attribute and the optional attribute must be a subset of its type
interface Person {
name: string;
age?: number;
[propName: string]: string;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Index signatures are incompatible.
// Type 'string | number' is not assignable to type 'string'.
// Type 'number' is not assignable to type 'string'.
In the above example, the value of any attribute is allowed to be string
, but the value age
of is number
, notnumber
a sub-attribute of , so an error is reported.string
In addition, it can be seen from the error message that the type { name: 'Tom', age: 25, gender: 'male' }
of { [x: string]: string | number; name: string; age: number; gender: string; }
, which is a combination of a joint type and an interface.
Only one arbitrary property can be defined in an interface. If you have properties of more than one type in the interface, you can use union types in any of the properties:
interface Person {
name: string;
age?: number; // 这里真实的类型应该为:number | undefined
[propName: string]: string | number | undefined;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
duck typing
The so-called duck-type identification method is 像鸭子一样走路并且嘎嘎叫的就叫鸭子
that it has the characteristics of a duck that it is a duck, that is, it is determined whether the object implements this interface by formulating rules.
example
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
let myObj = {
size: 10, label: "Size 10 Object" };
printLabel(myObj); // OK
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
printLabel({
size: 10, label: "Size 10 Object" }); // Error
In the above code, writing an object in the parameter is equivalent to directly assigning labeledObj
a value. This object has a strict type definition, so more or less parameters cannot be used. And when you use another variable to receive the object outside myObj
, myObj
it will not be checked for additional attributes, but it will be deduced according to the type let myObj: { size: number; label: string } = { size: 10, label: "Size 10 Object" };
, and then this will be myObj
assigned to labeledObj
. At this time, according to the compatibility of the type, two types of objects, refer to duck The type identification method , because they both have label
attributes, are considered to be the same as two, so this method can be used to bypass redundant type checks.
Ways to bypass extra property checks
duck typing
As shown in the example above
type assertion
The meaning of type assertion is equivalent to telling the program that you know what you are doing, and the program will naturally not perform additional property checks.
interface Props {
name: string;
age: number;
money?: number;
}
let p: Props = {
name: "兔神",
age: 25,
money: -100000,
girl: false
} as Props; // OK
index signature
interface Props {
name: string;
age: number;
money?: number;
[key: string]: any;
}
let p: Props = {
name: "兔神",
age: 25,
money: -100000,
girl: false
}; // OK
Case Analysis
Scenario: Declare a Child, Student, and Classroom classes respectively, and there is an instance method play in each class.
class Child {
name: string;
constructor(name: string) {
this.name = name;
}
play():void {
console.log(this.name + '正在玩耍');
}
}
class Student {
name: string;
constructor(name: string) {
this.name = name;
}
play():void {
console.log(this.name + '正在玩耍');
}
}
class Classroom {
play(who: Child):void {
who.play();
}
}
let child: Child = new Child('小明');
let stu: Student = new Student('小刚');
let room: Classroom = new Classroom();
// 小孩可以在教室玩耍
room.play(child);
// 学生也可以在教室玩耍,但是Classroom的play方法限制只有Child类才能进去玩耍
// 如果类型不匹配就会报错,Classroom类不合理,肿么办???
// room.play(stu);
Retrofit the Classroom class with an interface
// 声明一个接口
interface CanPlay {
name: string;
play(): void;
}
class Classroom {
// 当函数的参数类型不需要确定为某个具体的类,而是要限制其拥有哪些属性和方法,就可以用接口进行限制
play(who: CanPlay): void {
who.play();
}
}
// 声明类的时候可以用接口进行限制,声明这个类实现某个接口,那么这个类中就必须实现接口中声明的属性和方法,否则会报错
class Child implements CanPlay {
name: string;
constructor(name: string) {
this.name = name;
}
play(): void {
console.log(this.name + '正在玩耍');
}
}
class Student implements CanPlay {
name: string;
constructor(name: string) {
this.name = name;
}
play(): void {
console.log(this.name + '正在玩耍');
}
}
let child: Child = new Child('小明');
let stu: Student = new Student('小刚');
let room: Classroom = new Classroom();
room.play(child);
room.play(stu);
// 在方法中也可以使用接口限制参数的类型,只要是实现了CanPlay接口类的实例都可以传递进来
function wan(obj: CanPlay): void {
obj.play();
}
wan(child);
wan(stu);
Difference between interface and type alias
In fact, in most cases, the effects of using interface types and type aliases are equivalent, but there are still big differences between the two in some specific scenarios.
One of the core principles of TypeScript is type checking the structure a value has. The role of the interface is to name these types and define the data model for your code or third-party code.
type (type alias) gives a new name to a type. type is sometimes similar to interface, but can be applied to primitive values (primitive types), union types, tuples, and any other type you need to write by hand. Aliasing doesn't create a new type - it creates a new name to refer to that type. Aliasing primitive types is generally not useful, although it can be used as a form of documentation
Objects / Functions
Both can be used to describe the type of an object or function, but the syntax is different.
Interface
interface Point {
x: number;
y: number;
}
interface SetPoint {
(x: number, y: number): void;
}
Type alias
type Point = {
x: number;
y: number;
};
type SetPoint = (x: number, y: number) => void;
Other Types
Unlike interfaces, type aliases can also be used for other types such as primitive types (primitive values), union types, tuples.
// primitive
type Name = string;
// object
type PartialPointX = {
x: number; };
type PartialPointY = {
y: number; };
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
// dom
let div = document.createElement('div');
type B = typeof div;
Interfaces can be defined multiple times, type aliases cannot
Unlike type aliases, interfaces can be defined multiple times and are automatically merged into a single interface.
interface Point {
x: number; }
interface Point {
y: number; }
const point: Point = {
x: 1, y: 2 };
expand
The two scale differently, but are not mutually exclusive. Interfaces can extend type aliases, and type aliases can also extend interfaces.
The extension of the interface is inheritance, which is realized extends
through . The extension of type alias is cross type, which is realized &
by .
Interface extension interface
interface PointX {
x: number
}
interface Point extends PointX {
y: number
}
type aliases extend type aliases
type PointX = {
x: number
}
type Point = PointX & {
y: number
}
Interface extension type alias
type PointX = {
x: number
}
interface Point extends PointX {
y: number
}
type alias extension interface
interface PointX {
x: number
}
type Point = PointX & {
y: number
}
generic
Introduction to Generics
If you were asked to implement a function identity
whose parameter could be any value, and the return value was to return the parameter as it is, and it could only accept one parameter, what would you do?
You will think this is very simple, just write the code like this:
const identity = (arg) => arg;
Since it can accept any value, that is to say, the input parameter and return value of your function should be of any type. Now let's add type declarations to the code:
type idBoolean = (arg: boolean) => boolean;
type idNumber = (arg: number) => number;
type idString = (arg: string) => string;
...
A stupid method is like the above, that is to say, as many types as JS provides, you need to copy as many copies of the code, and then change the type signature. This is fatal for programmers. This kind of copying and pasting increases the probability of errors, makes the code difficult to maintain, and affects the whole body. And if new types are added to JS in the future, you still need to modify the code, which means that your code is open to modification , which is not good. Another way is to use the "universal syntax" of any. What are the disadvantages? Let me give you an example:
identity("string").length; // ok
identity("string").toFixed(2); // ok
identity(null).toString(); // ok
...
If you use any, whatever you write is ok, which loses the effect of type checking. In fact, I know that what I passed to you is a string, and the return must be a string, and there is no toFixed method on the string, so the need to report an error is what I want. In other words, the effect I really want is: 当我用到id的时候,你根据我传给你的类型进行推导
. For example, if I pass in a string, but use the method on number, you should report an error.
In order to solve the above problems, we use generics to refactor the above code . Different from our definition, a type T is used here, this T is an abstract type, and its value is only determined when it is called , so we don't need to copy and paste countless copies of code.
function identity<T>(arg: T): T {
return arg;
}
where T
stands for Type and is usually used as the first type variable name when defining generics. But virtually any valid name T
can be substituted. In T
addition to , the following are the meanings of common generic variables:
- K (Key): Indicates the key type in the object;
- V (Value): Indicates the value type in the object;
- E (Element): Indicates the element type.
Here is a picture to help you understand
In fact, we can not only define one type variable, we can introduce any number of type variables we want to define. For example, we introduce a new type variable U
to extend identity
the function :
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
Instead of explicitly setting values for type variables, it is more common to have the compiler choose these types automatically, resulting in cleaner code. We can omit the angle brackets entirely, like:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity(68, "Semlinker"));
For the above code, the compiler is smart enough to know the types of our parameters and assign them to T and U without the developer needing to specify them explicitly.
Generic constraints
What if I want to print out the size attribute of the parameter? If TS is not constrained at all, an error will be reported:
function trace<T>(arg: T): T {
console.log(arg.size); // Error: Property 'size doesn't exist on type 'T'
return arg;
}
The reason for the error is that T can be of any type in theory, unlike any, you will report an error no matter what property or method you use (unless this property and method are common to all collections). Then the intuitive idea is to limit the parameter type passed to the trace function to the size type, so that no error will be reported. How to express the point of this type constraint ? The key to achieving this requirement is to use type constraints. This can be done using the extends keyword. Simply put, you define a type, and then let T implement this interface.
interface Sizeable {
size: number;
}
function trace<T extends Sizeable>(arg: T): T {
console.log(arg.size);
return arg;
}
Some people may say that I can directly limit the parameters of Trace to Sizeable type? If you do this, there is a risk of type loss, please refer to this article A use case for TypeScript Generics for details .
generic utility type
For the convenience of developers, TypeScript has built-in some commonly used tool types, such as Partial, Required, Readonly, Record, and ReturnType. But before the specific introduction, we have to introduce some relevant basic knowledge first, so that readers can better learn other tool types.
1.typeof
The main purpose of typeof is to obtain the type of a variable or property in a type context. Let's understand it through a specific example.
interface Person {
name: string;
age: number;
}
const sem: Person = {
name: "semlinker", age: 30 };
type Sem = typeof sem; // type Sem = Person
In the above code, we get the type of the sem variable through typeof
the operator and assign it to the Sem type variable, then we can use the Sem type:
const lolo: Sem = {
name: "lolo", age: 5 }
You can also do the same with nested objects:
const Message = {
name: "jimmy",
age: 18,
address: {
province: '四川',
city: '成都'
}
}
type message = typeof Message;
/*
type message = {
name: string;
age: number;
address: {
province: string;
city: string;
};
}
*/
In addition, typeof
in addition to obtaining the structural type of the object, the operator can also be used to obtain the type of the function object, such as:
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
2.keyof
keyof
The operator was introduced in TypeScript 2.1. This operator can be used to get all the keys of a certain type, and its return type is a union type.
interface Person {
name: string;
age: number;
}
type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof {
[x: string]: Person }; // string | number
Two types of index signatures are supported in TypeScript, numeric indexes and string indexes:
interface StringArray {
// 字符串索引 -> keyof StringArray => string | number
[index: string]: string;
}
interface StringArray1 {
// 数字索引 -> keyof StringArray1 => number
[index: number]: string;
}
In order to support both index types at the same time, it is required that the return value of the numeric index must be a subclass of the return value of the string index. The reason is that when using a numeric index, JavaScript will first convert the numeric index to a string index when performing an index operation . So the result keyof { [x: string]: Person }
of will be returned string | number
.
keyof also supports basic data types:
let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K1: "valueOf"
The role of keyof
JavaScript is a highly dynamic language. Sometimes it can be tricky to capture the semantics of certain operations in a static type system. Take a simple prop
function as an example:
function prop(obj, key) {
return obj[key];
}
This function receives two parameters, obj and key, and returns the value of the corresponding property. Different properties on the object can have completely different types, and we don't even know what the obj object looks like.
So how to define the above prop
function ? Let's try it out:
function prop(obj: object, key: string) {
return obj[key];
}
In the above code, in order to avoid passing in the wrong parameter type when calling the prop function, we set the types for the obj and key parameters, which are {}
and string
types respectively. However, things are not that simple. For the above code, the TypeScript compiler will output the following error message:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{
}'.
Elements implicitly have any
the type because string
the type cannot be used to index {}
the type . To fix this, you can use the following very brute-force solution:
function prop(obj: object, key: string) {
return (obj as any)[key];
}
Obviously, this solution is not a good solution. Let's review the function of prop
the function , which is used to obtain the attribute value of a specified attribute in an object. Therefore, we expect that the attribute entered by the user is an attribute that already exists on the object, so how to limit the scope of the attribute name? At this time, we can use the protagonist keyof
operator :
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
In the above code, we have used TypeScript's generics and generic constraints. First define the T type and use extends
the keyword constraint that the type must be a subtype of the object type, then use keyof
the operator to get all the keys of the T type, and its return type is a union type, and finally use extends
the keyword constraint that the K type must be keyof T
a union type Subtype. You will know if it is a mule or a horse for a walk, let’s test it out:
type Todo = {
id: number;
text: string;
done: boolean;
}
const todo: Todo = {
id: 1,
text: "Learn TypeScript keyof",
done: false
}
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const id = prop(todo, "id"); // const id: number
const text = prop(todo, "text"); // const text: string
const done = prop(todo, "done"); // const done: boolean
Obviously, using generics, the redefined prop<T extends object, K extends keyof T>(obj: T, key: K)
function can already correctly deduce the type corresponding to the specified key. So what happens when you access a property that doesn't exist on the todo object? for example:
const date = prop(todo, "date");
For the above code, the TypeScript compiler will prompt the following error:
Argument of type '"date"' is not assignable to parameter of type '"id" | "text" | "done"'.
This prevents us from trying to read properties that don't exist.
3.in
in
Used to traverse enumerated types:
type Keys = "a" | "b" | "c"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }
4.infer
In a conditional type statement, you can infer
declare a type variable and use it.
type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any;
In the above code, a variable infer R
is declared to carry the return value type of the incoming function signature. Simply put, it is used to obtain the type of the function return value for later use.
5.extends
Sometimes the generics we define do not want to be too flexible or want to inherit certain classes, etc., we can add generic constraints through the extends keyword.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
Now the generic function is constrained so it no longer applies to any type:
loggingIdentity(3); // Error, number doesn't have a .length property
At this time, we need to pass in a value that conforms to the constraint type, which must contain the length attribute:
loggingIdentity({
length: 10, value: 3});
index type
In actual development, we often encounter such a scenario, get the value of some attributes in the object, and then create the corresponding collection.
let person = {
name: 'musion',
age: 35
}
function getValues(person: any, keys: string[]) {
return keys.map(key => person[key])
}
console.log(getValues(person, ['name', 'age'])) // ['musion', 35]
console.log(getValues(person, ['gender'])) // [undefined]
In the above example, you can see that getValues(persion, ['gender']) prints [undefined], but the ts compiler does not give an error message, so how to use ts to constrain the type of this mode? The index type will be used here, modify the getValues function, and use the index type query and index access operators:
function getValues<T, K extends keyof T>(person: T, keys: K[]): T[K][] {
return keys.map(key => person[key]);
}
interface Person {
name: string;
age: number;
}
const person: Person = {
name: 'musion',
age: 35
}
getValues(person, ['name']) // ['musion']
getValues(person, ['gender']) // 报错:
// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.
// Type "gender" is not assignable to type "name" | "age".
The compiler checks that the value passed in is part of Person. The above code can be understood by the following concepts:
T[K]表示对象T的属性K所表示的类型,在上述例子中,T[K][] 表示变量T取属性K的值的数组
// 通过[]索引类型访问操作符, 我们就能得到某个索引的类型
class Person {
name:string;
age:number;
}
type MyType = Person['name']; //Person中name的类型为string type MyType = string
After introducing the concept, you should be able to understand the above code. First look at generics. There are two types, T and K. According to type inference, the first parameter person is person, and the type will be inferred as Person. As for the type inference of the second array parameter (K extends keyof T), the keyof keyword can obtain T, which is all attribute names of Person, namely ['name', 'age']. The extends keyword allows the generic type K to inherit all attribute names of Person, namely ['name', 'age']. The combination of these three features ensures the dynamics and accuracy of the code, and also makes the code hints richer
getValues(person, ['gender']) // 报错:
// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.
// Type "gender" is not assignable to type "name" | "age".
mapping type
Create a new type based on the old type, we call it a mapped type
For example, we define an interface
interface TestInterface{
name:string,
age:number
}
We make all the properties in the interface defined above optional
// 我们可以通过+/-来指定添加还是删除
type OptionalTestInterface<T> = {
[p in keyof T]+?:T[p]
}
type newTestInterface = OptionalTestInterface<TestInterface>
// type newTestInterface = {
// name?:string,
// age?:number
// }
For example, we add read-only
type OptionalTestInterface<T> = {
+readonly [p in keyof T]+?:T[p]
}
type newTestInterface = OptionalTestInterface<TestInterface>
// type newTestInterface = {
// readonly name?:string,
// readonly age?:number
// }
Since it is common to generate read-only attributes and optional attributes, TS has provided us with a ready-made implementation of Readonly / Partial, and the built-in tool types will be introduced.
Built-in tool types
Partial
Partial<T>
Make a type property optional
definition
type Partial<T> = {
[P in keyof T]?: T[P];
};
In the above code, first keyof T
get T
all the attribute names of , then use in
to traverse, assign the value to P
, and finally T[P]
get . A middle ?
sign is used to make all attributes optional.
for example
interface UserInfo {
id: string;
name: string;
}
// error:Property 'id' is missing in type '{ name: string; }' but required in type 'UserInfo'
const xiaoming: UserInfo = {
name: 'xiaoming'
}
usePartial<T>
type NewUserInfo = Partial<UserInfo>;
const xiaoming: NewUserInfo = {
name: 'xiaoming'
}
This NewUserInfo is equivalent to
interface NewUserInfo {
id?: string;
name?: string;
}
But Partial<T>
there is a limitation, that is, it only supports processing the attributes of the first layer, if my interface definition is like this
interface UserInfo {
id: string;
name: string;
fruits: {
appleNumber: number;
orangeNumber: number;
}
}
type NewUserInfo = Partial<UserInfo>;
// Property 'appleNumber' is missing in type '{ orangeNumber: number; }' but required in type '{ appleNumber: number; orangeNumber: number; }'.
const xiaoming: NewUserInfo = {
name: 'xiaoming',
fruits: {
orangeNumber: 1,
}
}
It can be seen that after the second layer, it will not be processed. If you want to process multiple layers, you can implement it yourself
DeepPartial
type DeepPartial<T> = {
// 如果是 object,则递归类型
[U in keyof T]?: T[U] extends object
? DeepPartial<T[U]>
: T[U]
};
type PartialedWindow = DeepPartial<T>; // 现在T上所有属性都变成了可选啦
Required
Required turns the attributes of the type into mandatory
definition
type Required<T> = {
[P in keyof T]-?: T[P]
};
Where -?
is ?
the flag that represents removing this modifier. To expand, in addition to being applied to ?
this modifiers, there are also applications readonly
, such as Readonly<T>
this type
type Readonly<T> = {
readonly [p in keyof T]: T[p];
}
Readonly
Readonly<T>
The function of is to turn all properties of a certain type into read-only properties, which means that these properties cannot be reassigned.
definition
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
for example
interface Todo {
title: string;
}
const todo: Readonly<Todo> = {
title: "Delete inactive users"
};
todo.title = "Hello"; // Error: cannot reassign a readonly property
Pick
Pick picks out some attributes from a type
definition
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
for example
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
You can see that there is only one name attribute in NewUserInfo.
Record
Record<K extends keyof any, T>
The role of is to convert the values of all attributesK
in toT
the type.
definition
type Record<K extends keyof any, T> = {
[P in K]: T;
};
for example
interface PageInfo {
title: string;
}
type Page = "home" | "about" | "contact";
const x: Record<Page, PageInfo> = {
about: {
title: "about" },
contact: {
title: "contact" },
home: {
title: "home" },
};
ReturnType
Used to get the return type of a function
definition
type ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: any;
infer
Used here to extract the return type of the function type. ReturnType<T>
Just move infer R from the parameter position to the return value position, so at this time R represents the return value type to be inferred.
for example
type Func = (value: number) => string;
const foo: ReturnType<Func> = "1";
ReturnType
Func
The return value type obtained is string
, so foo
it can only be assigned as a string.
Exclude
Exclude<T, U>
The function is to remove the type belonging to another type in one type.
definition
type Exclude<T, U> = T extends U ? never : T;
If T
is assignable to U
the type, then never
the type , otherwise T
the type is returned. The final effect is T
to U
remove some of the types belonging to .
for example
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number
Extract
Extract<T, U>
The role of is to extractT
fromU
.
definition
type Extract<T, U> = T extends U ? T : never;
for example
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () =>void
Omit
Omit<T, K extends keyof any>
The function ofT
isK
to construct a new type using all attributes of the type except the type.
definition
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
for example
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Omit<Todo, "description">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
NonNullable
NonNullable<T>
The function of is to filter thenull
andundefined
.
definition
type NonNullable<T> = T extendsnull | undefined ? never : T;
for example
type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]
Parameters
Parameters<T>
The function is to obtain the tuple type composed of the parameter types of the function.
definition
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any
? P : never;
for example
type A = Parameters<() =>void>; // []
type B = Parameters<typeofArray.isArray>; // [any]
type C = Parameters<typeofparseInt>; // [string, (number | undefined)?]
type D = Parameters<typeofMath.max>; // number[]
Some tips for writing efficient TS code
minimize duplication of code
For those who are new to TypeScript, when defining an interface, the following similar repetitive code may accidentally appear. for example:
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate {
firstName: string;
lastName: string;
birth: Date;
}
Obviously, compared to Person
the interface , PersonWithBirthDate
the interface just has one more birth
attribute , and the other attributes Person
are the same as the interface. So how to avoid the duplication of code in the example? To solve this problem, you can take advantage of extends
the keyword :
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate extends Person {
birth: Date;
}
Of course, in addition to using extends
the keyword , you can also use the intersection operator (&):
type PersonWithBirthDate = Person & {
birth: Date };
Additionally, sometimes you may find yourself wanting to define a type to match the "shape" of an initial configuration object, such as:
const INIT_OPTIONS = {
width: 640,
height: 480,
color: "#00FF00",
label: "VGA",
};
interface Options {
width: number;
height: number;
color: string;
label: string;
}
In fact, for the Options interface, you can also use the typeof operator to quickly obtain the "shape" of the configuration object:
type Options = typeof INIT_OPTIONS;
In actual development, duplicate types are not always easy to spot. Sometimes they are masked by syntax. For example, there are multiple functions with the same type signature:
function get(url: string, opts: Options): Promise<Response> {
/* ... */ }
function post(url: string, opts: Options): Promise<Response> {
/* ... */ }
For the above get and post methods, in order to avoid duplication of code, you can extract a unified type signature:
type HTTPFunction = (url: string, opts: Options) => Promise<Response>;
const get: HTTPFunction = (url, opts) => {
/* ... */ };
const post: HTTPFunction = (url, opts) => {
/* ... */ };
Use a more precise type instead of a string type
Say you're building a music collection and want to define a genre for albums. At this point you can use interface
the keyword to define a Album
type:
interface Album {
artist: string; // 艺术家
title: string; // 专辑标题
releaseDate: string; // 发行日期:YYYY-MM-DD
recordingType: string; // 录制类型:"live" 或 "studio"
}
For Album
the type , you want releaseDate
the attribute value to be in the format of YYYY-MM-DD
and the range of recordingType
the attribute value to be live
or studio
. But releaseDate
because recordingType
the types of the and attributes in the interface are both strings, the following problems may occur when using Album
the interface :
const dangerous: Album = {
artist: "Michael Jackson",
title: "Dangerous",
releaseDate: "November 31, 1991", // 与预期格式不匹配
recordingType: "Studio", // 与预期格式不匹配
};
releaseDate
Although recordingType
the values of and don't match the expected format, the TypeScript compiler doesn't spot the problem at this point. To fix this, you releaseDate
should recordingType
define more precise types for the and properties, like this:
interface Album {
\
artist: string; // 艺术家
title: string; // 专辑标题
releaseDate: Date; // 发行日期:YYYY-MM-DD
recordingType: "studio" | "live"; // 录制类型:"live" 或 "studio"
}
After redefining Album
the interface , the TypeScript compiler will prompt the following exception information for the previous assignment statement:
const dangerous: Album = {
artist: "Michael Jackson",
title: "Dangerous",
// 不能将类型“string”分配给类型“Date”。ts(2322)
releaseDate: "November 31, 1991", // Error
// 不能将类型“"Studio"”分配给类型“"studio" | "live"”。ts(2322)\
recordingType: "Studio", // Error
};
In order to solve the above problem, you need releaseDate
to recordingType
set the correct type for the and properties, like this:
const dangerous: Album = {
artist: "Michael Jackson",
title: "Dangerous",
releaseDate: new Date("1991-11-31"),
recordingType: "studio",
};
A defined type always represents a valid state
Suppose you're building a web application that allows the user to specify a page number, then loads and displays the corresponding content for that page. First, you might define State
the object :
interface State {
pageContent: string;
isLoading: boolean;
errorMsg?: string;
}
Then you define a renderPage
function to render the page:
function renderPage(state: State) {
if (state.errorMsg) {
return `呜呜呜,加载页面出现异常了...${
state.errorMsg}`;
} else if (state.isLoading) {
return `页面加载中~~~`;
}
return `<div>${
state.pageContent}</div>`;
}
// 输出结果:页面加载中~~~
console.log(renderPage({
isLoading: true, pageContent: ""}));
// 输出结果:<div>大家好</div>
console.log(renderPage({
isLoading: false, pageContent: "大家好呀"}));
After creating renderPage
the function , you can continue to define a changePage
function to get the corresponding page data according to the page number:
async function changePage(state: State, newPage: string) {
state.isLoading = true;
try {
const response = await fetch(getUrlForPage(newPage));
if (!response.ok) {
throw new Error(`Unable to load ${
newPage}: ${
response.statusText}`);
}
const text = await response.text();
state.isLoading = false;
state.pageContent = text;
} catch (e) {
state.errorMsg = "" + e;
}
}
For the above changePage
function , it has the following problems:
- In the catch statement, the state
state.isLoading
of set tofalse
; state.errorMsg
The value of was not cleaned up in time , so if a previous request failed, you'll continue to see error messages instead of loading messages.
The above problem occurs because the previously defined State
type allows the values ofisLoading
and to be set at the same time, although this is an invalid state. errorMsg
To solve this problem, you can consider introducing a recognized union type to define different page request states:
interface RequestPending {
state: "pending";
}
interface RequestError {
state: "error";
errorMsg: string;
}
interface RequestSuccess {
state: "ok";
pageContent: string;
}
type RequestState = RequestPending | RequestError | RequestSuccess;
interface State {
currentPage: string;
requests: {
[page: string]: RequestState };
}
In the above code, 3 different request states are defined by using identifiable joint types, so that different request states can be easily distinguished, thus making the business logic processing clearer. Next, you need to update the previously created and functions based on the updated State
type :renderPage
changePage
Updated renderPage function
function renderPage(state: State) {
const {
currentPage } = state;
const requestState = state.requests[currentPage];
switch (requestState.state) {
case "pending":
return `页面加载中~~~`;
case "error":
return `呜呜呜,加载第${
currentPage}页出现异常了...${
requestState.errorMsg}`;
case "ok":
`<div>第${
currentPage}页的内容:${
requestState.pageContent}</div>`;
}
}
Updated changePage function
async function changePage(state: State, newPage: string) {
state.requests[newPage] = {
state: "pending" };
state.currentPage = newPage;
try {
const response = await fetch(getUrlForPage(newPage));
if (!response.ok) {
throw new Error(`无法正常加载页面 ${
newPage}: ${
response.statusText}`);
}
const pageContent = await response.text();
state.requests[newPage] = {
state: "ok", pageContent };
} catch (e) {
state.requests[newPage] = {
state: "error", errorMsg: "" + e };
}
}
In changePage
the function , different request states will be set according to different situations, and different request states will contain different information. In this way, renderPage
the function can state
perform corresponding processing according to the unified attribute value. Therefore, by using the Discriminated Union type, each state of the request is a valid state, and there will be no problem of invalid state.
More
Just talk but don't practice fake moves, click the link below to practice your hands