preface

This article has been written for several months. Before, I always studied in a piecemeal way, and then WROTE the article intermittently (lazy). I referred to a lot of articles and watched some TS videos, and then summarized all the basic knowledge points. I feel like I’m more detailed than all the popular typescript getting started articles on Nuggets, because I’ve referenced them all and covered most of them. This time it must settle. Finish this article well.

I did not like TS before, because it has a certain learning cost, increased code volume, code complexity, etc. I’ve come to think that TS’s static checking allows developers to detect errors in advance, which is really necessary in today’s front-end engineering world, where team members are of varying skill levels, and TypeScript helps prevent a lot of errors. If you’re a believer in any, I encourage you to be kind. Don’t use TypeScript for TypeScript’s sake. Use TypeScript only if it helps you solve a specific problem.

Advice: Don’t learn TypeScript because it’s cheap to learn don’t learn TypeScript because it reduces team inefficiency don’t learn TypeScript because it makes your code more robust don’t learn TypeScript, Don’t learn TypeScript because you’ll get hooked on it

~ alas, general trend, this trifle must learn now!

Introduce typescript

What is a typescript?

TypeScript is short for TS and the relationship between TS and JS is really the relationship between Less/Sass and CSS just as Less/Sass extends CSS, TS extends JS just as Less/Sass eventually converts to CSS, We write good TS code and we end up with JS. TypeScript is a superset of JavaScript because it extends JavaScript and has things that JavaScript doesn’t. In terms of parent-child relationships, TypeScript is a JavaScript subclass that extends from inheritance.

Why TypeScript?

Simply put, because JavaScript is weakly typed, many errors are discovered only at runtime, whereas TypeScript provides a static detection mechanism that helps detect errors at compile time

TypeScript characteristics

Support for the latest JavaScript features support for code static checking Support for features in back-end languages such as C,C++,Java,Go, etc. (enumerations, generics, type conversions, namespaces, declaration files, classes, interfaces, etc.)

Build a typescript learning environment

Install the latest version of typescript

npm i -g typescript
Copy the code

Ts – node installation

npm i -g ts-node
Copy the code

Create a tsconfig.json file

tsc --init
Copy the code

Then create a new index.ts, enter the relevant exercise code, and execute ts-Node index.ts

The official playground

Playground is also officially available as a cloud environment for developing TypeScript online.

It allows you to learn and write TypeScript in a browser without having to install the environment locally. It also allows you to easily select TypeScript versions, configure tsconfig, And TypeScript real-time static type detection, translation of JavaScript output, and online execution.

It’s also as good an experience as any native IDE, and a good choice for those of us just learning TypeScript.

Underlying data types

The eight built-in types of JS

let str: string = "jimmy";
let num: number = 24;
let bool: boolean = false;
let u: undefined = undefined;
let n: null = null;
let obj: object = {x: 1};
let big: bigint = 100n;
let sym: symbol = Symbol("me"); 
Copy the code

Pay attention to the point

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 and undefined are assigned to string
let str:string = "666";
str = null
str= undefined

// assign null and undefined to number
let num:number = Awesome!;
num = null
num= undefined

// null and undefined are assigned to object
let obj:object ={};
obj = null
obj= undefined

// assign null and undefined to Symbol
let sym: symbol = Symbol("me"); 
sym = null
sym= undefined

// assign null and undefined to Boolean
let isDone: boolean = false;
isDone = null
isDone= undefined

// assign null and undefined to bigint
let big: bigint =  100n;
big = null
big= undefined
Copy the code

If you specify “strictNullChecks” in tsconfig.json :true, null and undefined can only be assigned to void and their respective types.

The number and bigint

Although number and Bigint both represent numbers, the two types are incompatible.

let big: bigint =  100n;
let num: number = 6;
big = num;
num = big;
Copy the code

A type incompatible TS (2322) error is thrown.

Other types of

Array

Array types can be defined in two ways:

let arr:string[] = ["1"."2"];
let arr2:Array<string> = ["1"."2"];Copy the code

Define an associative type array

let arr:(number | string)[];
// define an array called arr,
// This array can store both numeric and string data in the future
arr3 = [1.'b'.2.'c'];
Copy the code

Defines an array of specified object members:

// interface is an interface
interface Arrobj{
    name:string,
    age:number
}
let arr3:Arrobj[]=[{name:'jimmy'.age:22}]
Copy the code

function

Function declaration

function sum(x: number, y: number) :number {
    return x + y;
}
Copy the code

Functional expression

let mySum: (x: number, y: number) = > number = function (x: number, y: number) :number {
    return x + y;
};
Copy the code

Use interfaces to define function types

interface SearchFunc{
  (source: string, subString: string): boolean;
}
Copy the code

When function expression interface is used to define functions, type restriction on the left side of equal sign can ensure that the number of parameters, parameter type and return value type remain unchanged when assigning values to function names in the future.

Optional parameters

function buildName(firstName: string, lastName? : string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        returnfirstName; }}let tomcat = buildName('Tom'.'Cat');
let tom = buildName('Tom');
Copy the code

Note: Optional parameters cannot be followed by required parameters

Parameter Default Value

function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom'.'Cat');
let tom = buildName('Tom');
Copy the code

The remaining parameters

function push(array: any[], ... items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}
let a = [];
push(a, 1.2.3);
Copy the code

Function overloading

Since JavaScript is a dynamic language, we usually call the same function with different types of arguments, and the function returns different types of results depending on the arguments:

function add(x, y) {
 return x + y;
}
add(1.2); / / 3
add("1"."2"); / / "12"
Copy the code

Since TypeScript is a superset of JavaScript, the above code can be used directly in TypeScript. However, when the TypeScript compiler turns on the noImplicitAny configuration item, this code prompts the following error message:

Parameter 'x' implicitly has an 'any' type.
Parameter 'y' implicitly has an 'any' type.
Copy the code

This information tells us that arguments x and y implicitly have type any. To solve this problem, we can set a type for the parameter. Because we want to add the function at the same time support the string and number types, so we can define a string | number joint type, at the same time we take individual name for the joint type:

type Combinable = string | number;
Copy the code

After defining the Combinable union type, let’s update the add function:

function add(a: Combinable, b: Combinable) {
    if (typeof a === 'string' || typeof b === 'string') {
     return a.toString() + b.toString();
    }
    return a + b;
}
Copy the code

When you explicitly type the parameters of the add function, the error message disappears. Now that the add function is perfect, let’s actually test it:

const result = add('Semlinker'.' Kakuqo');
result.split(' ');
Copy the code

In the above code, we call add with the strings ‘Semlinker’ and ‘Kakuqo’ and save the result to a variable named result, which we assume is of type string. So we can call the split method on the string object normally. The TypeScript compiler now displays the following error message:

Property 'split' does not exist on type 'number'.
Copy the code

It is clear that the split property does not exist on objects of type number. Here comes the problem. How to solve it? This is where we can take advantage of TypeScript’s function overloading features.

Function overloading or method overloading is the ability to create multiple methods with the same name and different numbers or types of arguments. The solution to the previous problem is to provide multiple function type definitions for the same function to override the function, and the compiler will process the function calls based on 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(' ');
Copy the code

In the above code, we provide multiple function type definitions for the Add function to enable function overloading. Then the nasty error message disappears again, because the result variable is of type string.

The Tuple (a Tuple)

As we all know, arrays are usually made up of values of the same type, but sometimes we need to store different types of values in a single variable, so we can use tuples. There are no tuples in JavaScript. Tuples are TypeScript specific types that work like arrays.

The most important feature of tuples is that they can limit the number and type of array elements, which makes them particularly suitable for multi-value returns.

Metadata is used to store data of fixed length and data type

let x: [string, number]; 
// The type must match and the number must be 2

x = ['hello'.10]; // OK 
x = ['hello'.10.10]; // Error 
x = [10.'hello']; // Error
Copy the code

Note that the tuple type can only represent an array with a known number and type of elements. The length is specified. If there are multiple types in an array, use any[].

void

Void denotes that there is no type, and is equal to other types, and cannot be assigned directly:

let a: void; 
let b: number = a; // Error
Copy the code

You can only give it null and undefined (if strictNullChecks is not true). Declaring a void variable is not very useful, and we usually only declare it when the function does not return a value.

Note that the method returns undefined without a value, but we need to define void instead of undefined. Otherwise an error will be reported:

function fun() :undefined {
  console.log("this is TypeScript");
};
fun(); // Error
Copy the code

never

The never type represents the types of values that never exist.

There are two cases where the value will never exist:

  1. If a function throws an exception, the function never returns (because throwing an exception directly interrupts the program, so that the program does not run until the return value step, i.e. has an unreachable end point, and never returns).
  2. Code in a function that executes an infinite loop (an infinite loop) such that the program never reaches the return value of the function, and never returns.
/ / exception
function err(msg: string) :never { // OK
  throw new Error(msg); 
}

/ / death cycle
function loopForever() :never { // OK
  while (true) {};
}

Copy the code

The never type, like null and undefined, is a subtype of any type and can be assigned to any type.

But no type is a subtype of never or can be assigned to never (other than never itself), and even any cannot be assigned to never

let ne: never;
let nev: never;
let an: any;

ne = 123; // Error
ne = nev; // OK
ne = an; // Error
ne = (() = > { throw new Error("Abnormal"); }) ();// OK
ne = (() = > { while(true) {} })(); // OK
Copy the code

In TypeScript, you can take advantage of the never feature to implement comprehensive checking, as shown in the following example:

type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // here foo is narrowed to string
  } else if (typeof foo === "number") {
    // here foo is narrowed to number
  } else {
    // foo is never
    constcheck: never = foo; }}Copy the code

Note that in the else branch, we assign foo, narrowed to never, to a variable that displays the declaration of never. If everything is logically correct, then this should compile. But suppose one day your colleague changes the type of Foo:

type Foo = string | number | boolean;
Copy the code

But he forgot to modify controlFlowAnalysisWithNever method in control process at the same time, this time the else branch foo type can be narrowed to a Boolean type, lead to cannot be assigned to never type, then will generate a compiler error. In this way, we can make sure controlFlowAnalysisWithNever method always end with all possible types of Foo. From this example, we can conclude that using never to avoid new union types with no corresponding implementation is the goal of writing type-safe code.

any

In TypeScript, any type can be classified as any. This makes the any type the top-level type of the type system.

If it is a common type, it is not allowed to change the type during assignment:

let a: string = 'seven';
a = 7;
// TS2322: Type 'number' is not assignable to type 'string'.
Copy the code

However, if it is of any type, it is allowed to be assigned to any type.

let a: any = Awesome!;
a = "Semlinker";
a = false;
a = 66
a = undefined
a = null
a = []
a = {}
Copy the code

It is allowed to access any property on any, and it is allowed to call 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');
Copy the code

A variable is recognized as any value type if its type is not specified when it is declared:

let something;
something = 'seven';
something = 7;
something.setName('Tom');
Copy the code

Is equivalent to

let something: any;
something = 'seven';
something = 7;
something.setName('Tom');
Copy the code

In many cases, this is too loose. Using the any type, you can easily write code that is typed correctly but has problems at run time. If we use any, we won’t be able to use the many protections TypeScript provides. Remember, any is the devil! Try not to use any.

To address the problem of any, TypeScript 3.0 introduces unknown types.

unknown

Like any, all types can be assigned to unknown:

let notSure: unknown = 4;
notSure = "maybe a string instead"; // OK
notSure = false; // OK
Copy the code

The main difference between unknown and any is that a value of any type can be assigned to any, and a value of any type can be assigned to any type. Unknown Any type of value can be assigned to it, but it can only be assigned to unknown and any

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

Copy the code

You cannot do anything with the unknown type without shrinking it:

function getDog() {
 return '123'
}
 
const dog: unknown = {hello: getDog};
dog.hello(); // Error
Copy the code

This mechanism is very preventive and more secure, which requires us to narrow down the type. We can use Typeof, type assertion, etc to narrow down the unknown:

function getDogName() {
 let x: unknown;
 return x;
};
const dogName = getDogName();
// Use it directly
const upName = dogName.toLowerCase(); // Error
// typeof
if (typeof dogName === 'string') {
  const upName = dogName.toLowerCase(); // OK
}
// Type assertion
const upName = (dogName as string).toLowerCase(); // OK
Copy the code

Number, String, Boolean, Symbol

First, let’s review the first TypeScript types that are easily confused with the primitive types number, string, Boolean, symbol. The latter are the wrapper objects of the corresponding primitive type, let’s call them object types.

In terms of type compatibility, primitive types are compatible with corresponding object types, and object types are incompatible with corresponding primitive types.

Let’s look at a concrete example:

let num: number;
let Num: Number;
Num = num; // ok
num = Num; / / ts (2322) an error
Copy the code

In line 3 of the example, we can assign number to the type number, but assigning number to number in line 4 prompts ts(2322) error.

Therefore, we need to remember not to use the object type to annotate the type of the value, because that doesn’t make any sense.

Object, object, and {}

In addition, object (lowercase, hereinafter called “small object”), object (uppercase, hereinafter called “large object”), and {} (hereinafter called “empty object”)

The little object represents all non-primitive types. That is, we cannot assign primitive types such as number, string, Boolean, and symbol to object. In strict mode, null and undefined cannot be assigned to object either.

The following types are treated as primitive types in JavaScript: String, Boolean, number, Bigint, Symbol, NULL, and undefined.

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
Copy the code

The ts(2322) error is displayed on lines 2 through 6 in the example, but we can pass static type detection after assigning an empty object to object on line 7.

Large Object represents all types that have toString and hasOwnProperty methods, so all primitive and non-primitive types can be assigned to Object. Similarly, null and undefined cannot be assigned to Object in strict mode.

Let’s also look at a concrete example:

let upperCaseObject: Object;
upperCaseObject = 1; // ok
upperCaseObject = 'a'; // ok
upperCaseObject = true; // ok
upperCaseObject = null; // ts(2322)
upperCaseObject = undefined; // ts(2322)
upperCaseObject = {}; // ok
Copy the code

In the example, lines 2 through 4, 7 pass static type detection, and lines 5 through 6 prompt ts(2322) errors.

As you can see from the example above, the large Object contains primitive types and the small Object contains only non-primitive types, so the large Object appears to be the parent of the small Object. In fact, a large Object is not only the parent type of a small Object, but also a child type of a small Object.

Let’s 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

Copy the code

In the example, lines 1 and 2 return type true, and upperCaseObject and lowerCaseObject can be assigned to each other on lines 3 and 4.

Note: Although the official documentation says you can use a small object instead of a large object, it is important to understand that a large object is not exactly equivalent to a small object.

{} empty Object types, like large objects, represent collections of primitive and non-primitive types, and null and undefined cannot be assigned to {} in strict mode, 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;
Copy the code

In this example, lines 8 and 9 return the type true. ObjectLiteral and upperCaseObject can be assigned to each other on lines 10 and 11. Assignments on lines 2 through 4 and 7 comply with static type checking. Lines 5 and 6 prompt ts(2322) error.

{} and large Object are more specific types than small objects (least specific). {} and large Object can be used to represent primitive types (except null and undefined) and non-primitive types. The little object represents a non-primitive type.

Type inference

{
  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;
}
Copy the code

Looking at the examples above, you’re probably already wondering: Isn’t TypeScript too cumbersome for variables that define base types to require type annotations? In our example, when using let to define a variable, we simply specify the type annotation because the value may change. However, when using const constants, you also need to specify the type annotation, which can be really cumbersome.

In fact, TypeScript already addresses this simple and obvious problem.

In many cases, TypeScript automatically infer the type of a variable based on the context, without requiring us to annotate the type. Therefore, the above example can be simplified to something like this:

{
  let str = 'this is string'; / / equivalent
  let num = 1; / / equivalent
  let bool = true; / / equivalent
}
{
  const str = 'this is string'; / / inequitable
  const num = 1; / / inequitable
  const bool = true; / / inequitable
}
Copy the code

We call TypeScript’s ability to infer types based on assigned expressions type inference.

In TypeScript, variables with initialized values, function parameters with default values, and types returned by functions can all be inferred from context. For example, we can infer the type of function return from the return statement, as shown in the following code:

{
  /** The type of the return value is number */
  function add1(a: number, b: number) {
    return a + b;
  }
  const x1= add1(1.1); // deduce that x1 is also of type number
  
  /** Assume that the type of parameter b is numeric or undefined, and the return value is numeric */
  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
}
Copy the code

If no assignment is made, it is inferred to be of type any and is not checked at all:

let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
Copy the code

Types of assertions

Sometimes you’ll find that you know more about a value than TypeScript does. Usually this happens when you clearly know that an entity has a more exact type than its existing type.

Type assertions are a way of telling the compiler, “Trust me, I know what I’m doing.” Type assertion is like conversion in other languages, but without special data checking and deconstruction. It has no run-time impact, only at compile time.

TypeScript type detection can’t be completely intelligent because programs can’t think like humans. There are times when we know more about the actual type than TypeScript does, as in the following example:

const arrayNumber: number[] = [1.2.3.4];
const greaterThan2: number = arrayNumber.find(num= > num > 2); / / hint ts (2322).
Copy the code

Where greaterThan2 must be a number (3, to be exact), because arrayNumber obviously has members greaterThan2, but static types are powerless against runtime logic.

In TypeScript’s view, greaterThan2 can be either a number or undefined, so the above example presents a TS (2322) error in which we cannot assign undefined to number.

However, we can tell TypeScript to do type checking the way we do, using type assertions (similar to casting only at the type level) in a declarative way.

For example, we can make type assertions using the AS syntax, as shown in the following code:

const arrayNumber: number[] = [1.2.3.4];
const greaterThan2: number = arrayNumber.find(num= > num > 2) as number;
Copy the code

grammar

// Angle bracket syntax
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

/ / as syntax
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
Copy the code

There is no difference between the two methods, but Angle brackets will clash with JSX in React, so we recommend using as syntax.

Not empty assertion

A new postfix expression operator in context when the type checker cannot determine the type! Can be used to assert that operation objects are non-null and non-undefined. Specifically, x! Null and undefined are excluded from the x range.

Take a look at the following examples:

let mayNullOrUndefinedOrString: null | undefined| string; mayNullOrUndefinedOrString! .toString();// ok
mayNullOrUndefinedOrString.toString(); // ts(2531)
Copy the code
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
  constnum2 = numGenerator! (a);//OK
}
Copy the code

Determine the assignment assertion

Allows placing one after instance attributes and variable declarations! To tell TypeScript that the property is explicitly assigned. To better understand what it does, let’s take a concrete example:

let x: number;
initialize();

// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
  x = 10;
}

Copy the code

The exception message clearly indicates that the variable x was used before the assignment. To resolve this problem, we can use a deterministic assignment assertion:

letx! : number; initialize();console.log(2 * x); // Ok

function initialize() {
  x = 10;
}
Copy the code

Through the let x! : number; Determine the assignment assertion, and the TypeScript compiler knows that the property is explicitly assigned.

Literal type

In TypeScript, literals can represent not only values but also types, known as literal types.

Currently, TypeScript supports three literal types: string literals, number literals, and Boolean literals. The corresponding string literals, number literals, and Boolean literals have the same literal type as their values, as shown in the following example:

{
  let specifiedStr: 'this is string' = 'this is string';
  let specifiedNum: 1 = 1;
  let specifiedBoolean: true = true;
}
Copy the code

For example, ‘this is string’ (which represents a string literal type) is of type string (or rather a subtype of string), A string need not be of the ‘this is string’ type, as shown in the following example:

{
  let specifiedStr: 'this is string' = 'this is string';
  let str: string = 'any string';
  specifiedStr = str; // ts(2322) type '"string"' cannot be assigned to type 'this is string'
  str = specifiedStr; // ok 
}
Copy the code

For example, we use “horse” to refer to the string type, that is, “black horse” refers to the type of “this is string”, “black horse” must be “horse”, but “horse” is not necessarily “black horse”, it may be “white horse” or “grey horse”. Thus, ‘this is string’ literals can assign to strings, but strings cannot assign to ‘this is string’ literals. The same analogy applies to numbers, boars, and other literals in relation to their parent classes.

String literal type

In general, we can use a string literal type as the type of a variable, as shown in the following code:

let hello: 'hello' = 'hello';
hello = 'hi'; // ts(2322) Type '"hi"' is not assignable to type '"hello"'
Copy the code

In fact, there is not much use in defining a single literal type. Its real use is when multiple literal types can be combined into a single union type (described below) that describes a practical collection with explicit members.

As shown in the code below, we use literal union types to describe an unambiguous, ‘up’, ‘down’ set so that we know exactly what data structures we need.

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'

Copy the code

By using the joint type of a combination of literal types, we can restrict function arguments to the specified set of literal types, and then the compiler checks whether the arguments are members of the specified set of literal types.

Therefore, using literal types (combined union types) allows you to restrict function arguments to more specific types than using strings. This not only improves the readability of the program, but also ensures that the parameter types of the function are used.

Numeric literal types and Boolean literal types

The use of numeric literals and Boolean literals is similar to the use of string literals. We can use joint types of literal combinations to limit function arguments to more specific types, such as declaring one of the following types, Config:

interface Config {
    size: 'small' | 'big';
    isEnable:  true | false;
    margin: 0 | 2 | 4;
}
Copy the code

In the above code, we limit the size attribute as a string literal type ‘small’ | ‘big’ isEnable properties for the type of Boolean literals true | false (Boolean literals true and false, only The combination of true | false with direct use of Boolean no difference), margin properties for numeric literals type 0 | 2 | 4.

Let and const analysis

Let’s start with 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
}
Copy the code

In the above code, we define const as an immutable constant. In the case of a default type annotation, iT makes sense for TypeScript to infer that its type is directly determined by the type of the assignment literal.

Let’s look at an example of a let like this:

{

  let str = 'this is string'; // str: string
  let num = 1; // num: number
  let bool = true; // bool: boolean
}
Copy the code

In the above code, the type of the mutable variable of the default explicit type annotation is converted to the parent of the assignment literal type, such as string, where STR is of type ‘this is String ‘(representing a string literal type). The type of num is number, the parent of type 1.

This works as expected, meaning that we can assign arbitrary values to STR and num (as long as the type is a subset of string and number) :

  str = 'any string';
  num = 2;
  bool = false;
Copy the code

The design of converting TypeScript literal quantum types into parent types is called “Literal design design”, which is for the creation of a variety of literal types, such as the string literal type mentioned in the previous example, for the creation of strings.

4) Type broadening (Type KP)

All variables defined by let or var, function parameters, and non-read-only attributes of an object that meet the conditions for specifying an initial value and not explicitly adding a type annotation, infer the type of the specified initial value literal-type broadening. This is literal-type broadening.

Let’s use the string literals example to understand literal type broadening:

  let str = 'this is string'; // The type is string
  let strFun = (str = 'this is string') = > str; // The type is (STR? : string) => string;
  const specifiedStr = 'this is string'; // The type is 'this is string'
  let str2 = specifiedStr; // The type is 'string'
  let strFun2 = (str = specifiedStr) = > str; // The type is (STR? : string) => string;
Copy the code

For 1 ~ 2 satisfied let, parameter and the condition of not explicitly declared type annotation, widened so variable, parameter type to string (parameter type is exactly in string | undefined).

Because the constants in line 3 are immutable, the type is not widened, so specifiedStr is of type ‘this is string’ literal type.

In lines 4 to 5, the types of variables and parameters are widened because the value specifiedStr is of type literal and has no explicit type annotation. In fact, such a design conforms to the actual programming appeal. Let’s imagine that if the type of STR2 is inferred to be ‘this is string’, it will be immutable because assigning any other value to a string type will prompt a type error.

Based on the conditions for literal type widening, we can control the type widening behavior by adding display type annotations as shown below.

{
  const specifiedStr: 'this is string' = 'this is string'; // Type is '"this is string"'
  let str2 = specifiedStr; // Even if let is used, the type is 'this is string'
}
Copy the code

In fact, in addition to the literal Type broadening, TypeScript has a design for certain types of values that is similar to the “Type KP “design, let’s look at it in detail.

For example, the type of null and undefined are broadened, and the type of these variables is inferred to be any if they satisfy the unexplicitly declared type annotation and are given null or undefined:

{
  let x = null; // Broaden the type to any
  let y = undefined; // Broaden the type to any

  /** ----- dividing line ------- */
  const z = null; // Type is null

  /** ----- dividing line ------- */
  let anyFun = (param = null) = > param; // The parameter type is null
  let z2 = z; // Type is null
  let x2 = x; // Type is null
  let y2 = y; // The type is undefined
}

Copy the code

Note: In strict mode, null and undefined are not widened to “any” in older versions (2.0).

Since Type widening is played, Type Narrowing will naturally occur. Below, we will briefly introduce Type Narrowing.

Type Narrowing

In TypeScript, we can narrow a variable’s Type from a relatively broad set to a relatively small and specific set by some operations. This is a role of “Type Narrowing”.

For example, we can use type guards (described below) to reduce the type of a function argument from any to an explicit type, as shown in the following example:

{
  let func = (anything: any) = > {
    if (typeof anything === 'string') {
      return anything; // The type is string
    } else if (typeof anything === 'number') {
      return anything; // The type is number
    }
    return null;
  };
}
Copy the code

In VS Code, hover starts at line 4 with an anything prompt of type string and starts at line 6 with a number.

Similarly, we can use type guards to narrow federated types down to explicit subtypes, as shown in the following example:

{
  let func = (anything: string | number) = > {
    if (typeof anything === 'string') {
      return anything; // The type is string
    } else {
      return anything; // The type is number}}; }Copy the code

Of course, we can also converge the union type to a more specific type through literal type equivalence judgment (===) or other control-flow statements (including but not limited to if, trinary operators, switch branches), as shown in the following code:

{
  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'}}}Copy the code

In the getCost function, the parameter type is a combination of literal types. The function contains three process branches of the IF statement, where each process branch calls a function whose parameters are specific independent literal types.

Why can a variable item whose type consists of multiple literals be passed to functions getPenCost, getPencilCost, and getRulerCost that accept only a single specific literal type? This is because in each process branch, the compiler knows what type item is in the process branch. For example, if item === is a 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 convergent type, as shown in the following code:

  const getCost = (item: Goods) = >  {
    if (item === 'pen') {
      item; // item => 'pen'
    } else {
      item; // => 'pencil' | 'ruler'}}Copy the code

The joint type

Joint type said value can be one of many types, using | separated for each type.

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven'; // OK
myFavoriteNumber = 7; // OK
Copy the code

Union types are usually used with null or undefined:

const sayHello = (name: string | undefined) = > {
  / *... * /
};
Copy the code

Here, for example, the type of the name is the string | undefined means that can be a string or undefined values passed to the sayHello function.

sayHello("semlinker"); 
sayHello(undefined);
Copy the code

From this example, you can intuitively know that the combined type of type A and type B is A type that accepts both A and B values. In addition, for union types, you may encounter the following usages:

let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';
Copy the code

The 1, 2, or ‘click’ in the examples above are called literal types that constrain the value to one of several values.

Type the alias

A type alias is used to give a type a new name. Type aliases are often used to combine types.

type Message = string | string[];
let greet = (message: Message) = > {
  // ...
};
Copy the code

Note: The type alias, like its name, is simply a new name for the type, not a new type.

Cross type

Cross typing is merging multiple types into one type. This allows us to superimpose existing types into a single type that contains all the required features of the type, using & to define the crossover type.

{
  type Useless = string & number;
}
Copy the code

Obviously, it would be useless if we simply merged primitive, literal, function, and other atomic types into crossover types, because no type can be a combination of multiple atomic types, such as string and number. Thus, in the above code, the Useless type alias is a never.

The real use for crossing types is to combine multiple interface types into a single type to achieve the same effect as interface inheritance, known as merging interface types, as shown in the following code:

  type IntersectionType = { id: number; name: string; } and {age: number };
  const mixed: IntersectionType = {
    id: 1.name: 'name'.age: 18
  }
Copy the code

In the above example, IntersectionType can have all the attributes of ID, name and age by crossing types. Here, we can try to understand the merged interface types as union.

thinking

Here, let’s think about what happens if you merge multiple interface types with the same attribute.

If the name attribute type is incompatible, such as in the example above where the name attribute type of the two interface types is number and string, then the name attribute type is the intersection of the atomic types number and string, that is, never. The following code looks like this:

  type IntersectionTypeConfict = { id: number; name: string; } and {age: number; name: number; };
  const mixedConflict: IntersectionTypeConfict = {
    id: 1.name: 2.// ts(2322) error, 'number' cannot be assigned to 'never'
    age: 2
  };
Copy the code

If we assign any type of name value to mixedConflict, we will get a type error. If we do not set the name attribute, we will get an error that the required name attribute is missing. In this case, it means that the IntersectionTypeConfict type crossed out in the above code is a useless type.

If an attribute of the same name has a compatible type, such as number, a subtype of number, or a numeric literal type, then the merged name attribute is a subtype of either.

The type of the name attribute in the following example is the numeric literal type 2, so we cannot assign any value other than 2 to the name attribute.

  type IntersectionTypeConfict = { id: number; name: 2; } and {age: number; name: number; };

  let mixedConflict: IntersectionTypeConfict = {
    id: 1.name: 2.// ok
    age: 2
  };
  mixedConflict = {
    id: 1.name: 22.// Type '22' cannot be assigned to type '2'
    age: 2
  };
Copy the code

Union, cross combination

Joint operator | priority & below the cross operator, also, we can through the use of the small bracket () to adjust the priority of the operators.

 // The crossover operator takes precedence over the union operator
  type UnionIntersectionA = { id: number; } and {name: string; } | { id: string; } and {name: number; };
  
 // Adjust the priority
  type UnionIntersectionB = ('px' | 'em' | 'rem' | The '%') | ('vh' | 'em' | 'rem' | 'pt');

Copy the code

Further, we can also introduce basic rules such as allocation rates and commutation laws into type combinations to optimize for cleaner, cleaner types, as shown in the following code:

  type UnionIntersectionC = ({ id: number; } and {name: string; } | { id: string; & {})name: number; };

  type UnionIntersectionD = { id: number; } and {name: string; } and {name: number; } | { id: string; } and {name: number; }; // Meet the allocation ratio

  type UnionIntersectionE = ({ id: string; } | { id: number; } and {name: string; & {})name: number; }; // It satisfies the commutative law

Copy the code

In the code above, line 2 expands on line 1, stating that & satisfies the distribution rate; Line 3 is based on 1 line to adjust the order of members, | operation is meet the exchange law.

Interfaces

In TypeScript, we use Interfaces to define the types of objects.

What is an interface

Interfaces are an important concept in object-oriented languages. They are an abstraction of behavior, and that behavior needs to be implemented by classes.

Interfaces in TypeScript are a very flexible concept. In addition to abstracting some of the behavior of a class, interfaces are often used to describe shapes of objects.

Simple example

interface Person {
    name: string;
    age: number;
}
let tom: Person = {
    name: 'Tom'.age: 25
};
Copy the code

In the example above, we define an interface Person, followed by a variable Tom, whose type is Person. Thus, we are constrained that the shape of Tom must be the same as the interface Person.

Interfaces generally start with uppercase letters.

It is not allowed to define a variable with fewer attributes than the interface:

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; } '.
Copy the code

Additional attributes are also disallowed:

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'.
Copy the code

As you can see, when assigning a value, the shape of the variable must match the shape of the interface.

Optional | read-only property

interface Person { readonly name: string; age? : number; }Copy the code

The read-only attribute is used to restrict the value of an object to being changed only when it is newly created. TypeScript also provides ReadonlyArray

, which is similar to Array

except that all mutable methods are removed, ensuring that arrays can never be modified after they are 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!
Copy the code

Any attribute

Sometimes we want an interface to allow arbitrary attributes in addition to mandatory and optional attributes, and we can use the form of index signatures to meet this requirement.

interface Person {
    name: string; age? : number; [propName: string]: any; }let tom: Person = {
    name: 'Tom'.gender: 'male'
};
Copy the code

Note that once any attribute is defined, the type of both the determined 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'.
Copy the code

In this example, the value of any attribute can be string, but the value of the optional attribute age is number. Number is not a child of string, so the error is reported.

In addition, it can be seen in the error message when {name: ‘Tom’, age: 25, gender: ‘male’} type inference into {[x: string] : string | number; name: string; age: number; gender: string; }, which is a combination of the union type and interface.

Only one arbitrary attribute can be defined in an interface. If the interface has more than one type of attribute, you can use the union type in any attribute:

interface Person {
    name: string; age? : number;/ / here real types should be: number | is undefined
    [propName: string]: string | number | undefined;
}

let tom: Person = {
    name: 'Tom'.age: 25.gender: 'male'
};
Copy the code

Duck type discrimination

The so-called duck type identification method is to walk like a duck and quack is called a duck, that is, the characteristics of a duck think it is a duck, that is, through the formulation of rules to determine whether the object to achieve this interface.

example

interface LabeledValue {
  label: string;
}
function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}
let myObj = { size: 10.label: "Size 10 Object" };
printLabel(myObj); // OK
Copy the code
interface LabeledValue {
  label: string;
}
function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}
printLabel({ size: 10.label: "Size 10 Object" }); // Error
Copy the code

In the above code, writing an object as a parameter is equivalent to assigning a value directly to labeledObj. This object has a strict type definition, so it cannot take more or less arguments. When you receive the object from outside with another variable myObj, myObj does not undergo additional property checks, but is inferred from type to let myObj: {size: number; label: string } = { size: 10, label: “Size 10 Object” }; , and then assign the myObj to labeledObj. At this time, according to the compatibility of types, the two types of objects are identified as the same as the duck type discrimination method because they both have the label attribute. Therefore, this method can be used to bypass the unnecessary type checking.

A way to bypass additional property checks

Duck type discrimination

As shown in the example above

Types of assertions

Type assertions tell the program that you know what you’re doing, and the program doesn’t need to do additional attribute checking.

interface Props { 
  name: string; age: number; money? : number; }let p: Props = {
  name: "Gods".age: 25.money: -100000.girl: false
} as Props; // OK
Copy the code

The index sign

interface Props { 
  name: string; age: number; money? : number; [key: string]: any; }let p: Props = {
  name: "Gods".age: 25.money: -100000.girl: false
}; // OK
Copy the code

The difference between interface and type aliases

In fact, using interface types and type aliases is equivalent in most cases, but there are significant differences between the two in certain scenarios.

One of TypeScript’s core tenets is type-checking of the structure a value has. Interfaces are used to name these types and define data models for your code or third-party code.

Type (type alias) gives a type a new name. Type is sometimes similar to interface, but can work with primitive values (primitives), union types, tuples, and anything else you need to write by hand. Aliasing does not create a new type – it creates a new name to refer to that type. Aliasing basic types is usually useless, 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;
}

Copy the code

Type alias

type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) = > void;

Copy the code

Other Types

Unlike interfaces, type aliases can also be used for other types, such as primitive types (primitive values), union types, and 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;
Copy the code

Interfaces can be defined more than once, but 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 };
Copy the code

extension

The extensions are different, but not mutually exclusive. In the same way that an interface extends a type alias, a type alias extends an interface.

An extension of an interface is inheritance, which is implemented through extends. An extension of a type alias is a cross type, implemented with &.

Interface Extended interface

interface PointX {
    x: number
}

interface Point extends PointX {
    y: number
}
Copy the code

Type alias Extension type alias


type PointX = {
    x: number
}

type Point = PointX & {
    y: number
}
Copy the code

Interface extension type alias

type PointX = {
    x: number
}
interface Point extends PointX {
    y: number
}
Copy the code

Type alias extension interface

interface PointX {
    x: number
}
type Point = PointX & {
    y: number
}
Copy the code

The generic

Generic introduction

What would you do if you were to implement a function identity that takes any value as an argument, returns the parameter as it is, and accepts only one parameter?

You’d think it would be easy to just write code like this:

const identity = (arg) = > arg;
Copy the code

Since it can take any value, that means your function’s input and return values should be of any type. Now let’s add a type declaration to our code:

type idBoolean = (arg: boolean) = > boolean;
type idNumber = (arg: number) = > number;
type idString = (arg: string) = >string; .Copy the code

A clumsy approach like the one above means that as many types as JS provides, you need to copy the code and change the type signature. This is fatal to programmers. This kind of copy-and-paste increases the chance of errors, makes code difficult to maintain, and makes it all the more important. And in the future when JS adds new types, you still need to modify the code, which means 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.Copy the code

If you use any, everything is OK and you lose the effect of type checking. In fact, I know that I sent you a string, and it must return a string, and there is no toFixed method on string, so I need to report an error. In other words, what I really want is that when I use an ID, you derive it from the type I passed to you. For example, if I pass in a string, but use a method on number, you should report an error.

To solve these problems, we refactored the code above using generics. In contrast to our definition, we use a type T, which is an abstract type whose value is determined only when it is called, so we don’t have to copy and paste countless copies of code.

function identity<T> (arg: T) :T {
  return arg;
}
Copy the code

Where T stands for Type and is usually used as the first Type variable name when defining generics. But T can actually be replaced by any valid name. In addition to T, here’s what common generic variables stand for:

  • K (Key) : indicates the Key type of the object.
  • V (Value) : indicates the Value type of an object.
  • E (Element) : indicates the Element type.

Here’s a picture to help you understand

Instead of defining only one type variable, we can introduce as many type variables as we wish. For example, we introduce a new type variable U to extend our identity function:

function identity <T.U> (value: T, message: U) : T {
  console.log(message);
  return value;
}
console.log(identity<Number, string>(68."Semlinker"));
Copy the code

In addition to explicitly setting values for type variables, it is more common to have the compiler automatically select these types to make the code cleaner. We can omit the Angle brackets entirely, as in:

function identity <T.U> (value: T, message: U) : T {
  console.log(message);
  return value;
}
console.log(identity(68."Semlinker"));
Copy the code

For the code above, the compiler was smart enough to know our parameter types and assign them to T and U without the developer specifying them explicitly.

Generic constraint

What if I wanted to print out the size property of the parameter? TS will report an error if the constraint is not applied at all:

function trace<T> (arg: T) :T {
  console.log(arg.size); // Error: Property 'size doesn't exist on type 'T'
  return arg;
}
Copy the code

The reason for the error is that T can theoretically be of any type, and unlike any, you will get an error no matter what property or method you use (unless the property or method is common to all collections). The intuitive idea is to limit the type of argument passed to trace to size so that no errors are reported. How do I express this point of type constraint? The key to implementing this requirement is the use of type constraints. You can do this using the extends keyword. Basically, you define a type and let T implement the interface.

interface Sizeable {
  size: number;
}
function trace<T extends Sizeable> (arg: T) :T {
  console.log(arg.size);
  return arg;
}
Copy the code

Some people might say can I just limit the Trace parameter to Sizeable? If you do this, you run the risk of type loss. See the article A Use Case for TypeScript Generics for details.

Generic tool types

Common tool types such as Partial, Required, Readonly, Record, and ReturnType are built into TypeScript for developers’ convenience. But before we go into details, let’s first introduce some of the basics so that readers can learn more about other tool types.

1.typeof

The primary use of typeof is to get the typeof a variable or property in a type context, as illustrated in a concrete example.

interface Person {
  name: string;
  age: number;
}
const sem: Person = { name: "semlinker".age: 30 };
type Sem = typeof sem; // type Sem = Person
Copy the code

In the above code, we get the typeof the sem variable using the typeof operator and assign it to the sem type variable, after which we can use the sem type:

const lolo: Sem = { name: "lolo".age: 5 }
Copy the code

You can do the same for nested objects:

const Message = {
    name: "jimmy".age: 18.address: {
      province: 'sichuan'.city: 'chengdu'   
    }
}
type message = typeof Message;
/* type message = { name: string; age: number; address: { province: string; city: string; }; } * /
Copy the code

In addition to retrieving the structural typeof an object, the typeof operator can also be used to retrieve the typeof a function object, for example:

function toArray(x: number) :Array<number> {
  return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
Copy the code

2.keyof

The keyof operator, introduced in TypeScript 2.1, can be used to get all keys of a type whose 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
Copy the code

There are two index signatures supported in TypeScript, numeric and string:

interface StringArray {
  / / string index - > keyof StringArray = > string | number
  [index: string]: string; 
}

interface StringArray1 {
  -> keyof StringArray1 => number
  [index: number]: string;
}
Copy the code

To support both index types, it is required that the numeric index return value be a subclass of the string index return value. The reason for this is that when using numeric indexes, JavaScript converts the numeric index to a string index before performing an index operation. So keyof {[x: string] : Person} returns the result of the 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"
Copy the code

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:

function prop(obj, key) {
  return obj[key];
}
Copy the code

This function takes the obj and key arguments and returns the value of the corresponding attribute. Different properties on objects can have completely different types, and we don’t even know what an OBj object looks like.

So how do we define the above prop functions in TypeScript? Let’s try it:

function prop(obj: object, key: string) {
  return obj[key];
}
Copy the code

In the above code, we set the obj and key arguments to {} and string types, respectively, to avoid passing in the wrong parameter types when calling prop functions. However, it’s not that simple. For the above code, the TypeScript compiler prints the following error message:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
Copy the code

The element implicitly has type any because string cannot be used to index the {} type. To solve this problem, you can use the following very violent solutions:

function prop(obj: object, key: string) {
  return (obj as any)[key];
}
Copy the code

Obviously this is not a good solution, so let’s review the role of the prop function, which is used to get the property value of a specified property in an object. So we expect the user to enter properties that already exist on the object, so how do we limit the range of property names? Here we can use the keyof operator, the main character of this article:

function prop<T extends object.K extends keyof T> (obj: T, key: K) {
  return obj[key];
}
Copy the code

In the above code, we use TypeScript generics and generic constraints. We define T and use the extends keyword to constrain that the type must be a subtype of object. We then use the keyof operator to get all keys of type T, whose return type is the union type. Finally, the extends keyword constrains K to be a subtype of the keyof T union type. Is the mule or the horse out for a walk to know, let’s actually test:

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
Copy the code

It is clear that using generics, the redefined prop

(obj: T, key: K) functions have been able to correctly deduce the type of the specified key. So what happens when you access properties that don’t exist on todo objects? Such as:

const date = prop(todo, "date");
Copy the code

For the above code, the TypeScript compiler prompts the following error:

Argument of type '"date"' is not assignable to parameter of type '"id" | "text" | "done"'.
Copy the code

This prevents us from trying to read properties that don’t exist.

3.in

In is used to iterate over enumerated types:

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }
Copy the code

4.infer

In conditional type statements, a type variable can be declared and used with infer.

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;
Copy the code

Infer R in the code above means to declare a variable to carry the return value type of the signature of the incoming function. In short, infer R means to use it to fetch the return value type of the function.

5.extends

Sometimes we define generics that don’t want to be too flexible or want to inherit classes, etc., and 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;
}
Copy the code

Now the generic function is constrained, so it no longer applies to any type:

loggingIdentity(3);  // Error, number doesn't have a .length property
Copy the code

We need to pass in a value that matches the constraint type, which must contain the length attribute:

loggingIdentity({length: 10.value: 3});
Copy the code

The index type

In real development, we often encounter scenarios where we get the values of some attributes in an object and then create corresponding collections.

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]
Copy the code

In the above example, getValues(persion, [‘gender’]) prints [undefined], but the TS compiler does not give an error message. How to use TS to constrain this schema? Here we use the index type. Modify the getValues function to use the index type query and the index access operator:

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']) / / error:
// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.
// Type "gender" is not assignable to type "name" | "age".

Copy the code

The compiler checks whether the value passed in is part of Person. To understand the above code, use the following concepts:

T[K] represents the type represented by the attribute K of the object T. In the above example, T[K][] represents the array of values of the variable T from the attribute KWe can get the type of an index by using the [] index type access operator
class Person {
    name:string;
    age:number;
 }
 type MyType = Person['name'];  // Name in Person is of type string type MyType = string

Copy the code

After introducing the concepts, you should be able to understand the code above. So let’s look at generics, there are two types, T and K, and by type inference, the first argument person is person, and the type is inferred to be Person. The second array argument type inference (K extends Keyof T). The keyof keyword retrieves T, which is the name of all the attributes of Person, i.e. [‘name’, ‘age’]. The extends keyword lets the generic K inherit all attribute names of Person, namely [‘name’, ‘age’]. The combination of these three features makes the code dynamic and accurate, and also makes the code hints richer

getValues(person, ['gender']) / / error:
// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.
// Type "gender" is not assignable to type "name" | "age".
Copy the code

Mapping type

New types are created from old types, which we call mapping types

Let’s say we define an interface

interface TestInterface{
    name:string,
    age:number
}
Copy the code

We make all the attributes in the interface defined above optional

// We can specify add or remove with +/-

type OptionalTestInterface<T> = {
  [p inkeyof T]+? :T[p] } type newTestInterface = OptionalTestInterface<TestInterface>// type newTestInterface = {
// name? :string,
// age? :number
// }
Copy the code

Let’s say we add read-only


type OptionalTestInterface<T> = {
 +readonly [p inkeyof T]+? :T[p] } type newTestInterface = OptionalTestInterface<TestInterface>// type newTestInterface = {
// readonly name? :string,
// readonly age? :number
// }
Copy the code

Since generating read-only and optional properties is common, TS already provides Readonly/Partial implementations. The built-in tool types are described.

Built-in tool type

Partial

Partial

makes the properties of the type optional

define

type Partial<T> = {
  [P inkeyof T]? : T[P]; };Copy the code

In the above code, first get all the attribute names of T by keyof T, then use in to traverse, assign the value to P, and finally get the corresponding attribute value class by T[P]. In the middle? Number to make all properties 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'
}
Copy the code

The use of Partial < T >

type NewUserInfo = Partial<UserInfo>;
const xiaoming: NewUserInfo = {
    name'xiaoming'
}
Copy the code

So this NewUserInfo is equivalent to

interface NewUserInfo { id? : string; name? : string; }Copy the code

However, Partial

has a limitation that it only supports layer 1 properties 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,}}Copy the code

As you can see, the second layer is not processed after that, and if you want to process multiple layers, you can implement it yourself

DeepPartial

type DeepPartial<T> = {
     // If object is a recursive type
    [U inkeyof T]? : T[U]extends object
      ? DeepPartial<T[U]>
      : T[U]
};

type PartialedWindow = DeepPartial<T>; // Now all attributes on T are optional
Copy the code

Required

Required makes the attributes of the type mandatory

define

type Required<T> = { 
    [P inkeyof T]-? : T[P] };Copy the code

Among them -? Does it stand for removal? The modifier logo. And I want to extend that a little bit, except that it can be applied to? This modifiers can also be used with readonly, such as readonly

type Readonly<T> = {
    readonly [p in keyof T]: T[p];
}
Copy the code

Readonly

Readonly

makes all properties of a type read-only, which means they cannot be reassigned.

define

type Readonly<T> = {
 readonly [P in keyof T]: T[P];
};
Copy the code

For example

interface Todo {
 title: string;
}

const todo: Readonly<Todo> = {
 title: "Delete inactive users"
};

todo.title = "Hello"; // Error: cannot reassign a readonly property
Copy the code

Pick

Pick picks out properties from a type

define

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
Copy the code

For example

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room".completed: false};Copy the code

You can see that there is only one name attribute in NewUserInfo.

Record

Record

converts the values of all properties in K to type T.

define

type Record<K extends keyof any, T> = {
    [P in K]: T;
};
Copy the code

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"}};Copy the code

ReturnType

The return value type used to get a function

define

type ReturnType<T extends(... args: any[]) => any> = Textends (
  ...args: any[]
) => infer R
  ? R
  : any;
Copy the code

Infer is used here to extract the return value types of function types. ReturnType

only moves infer R from the parameter position to the return value position, so R represents the return value type to be inferred.

For example

type Func = (value: number) = > string;
const foo: ReturnType<Func> = "1";
Copy the code

Func returns a string value, so foo can only be assigned as a string.

Exclude

Exclude

removes a type that belongs to another type.
,>

define

type Exclude<T, U> = T extends U ? never : T;
Copy the code

Never is returned if T can be assigned to U, T is returned otherwise. The net effect is to remove certain types of U from T.

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
Copy the code

Extract

Extract

Extract U from T
,>

define

type Extract<T, U> = T extends U ? T : never;
Copy the code

For example

type T0 = Extract<"a" | "b" | "c"."a" | "f">; // "a"
type T1 = Extract<string | number | (() = > void), Function>; // () =>void
Copy the code

Omit

Omit

is used to construct a new type using all properties of type T except type K.
,>

define

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Copy the code

For example

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
  title: "Clean room".completed: false};Copy the code

NonNullable

NonNullable

is used to filter null and undefined types.

define

type NonNullable<T> = T extendsnull | undefined ? never : T;
Copy the code

For example

type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]
Copy the code

Parameters

Parameters

is used to get the type of a tuple of the function’s parameter types.

define

type Parameters<T extends(... args: any) => any> = Textends(... args: infer P) => any ? P : never;Copy the code

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[]
Copy the code

tsconfig.json

Tsconfig. Json is introduced

Tsconfig. json is the configuration file for TypeScript projects. The existence of a tsconfig.json file in a directory usually means that the directory is the root of the TypeScript project.

Tsconfig. json contains configuration related to TypeScript compilation. By changing compilation configuration items, we can make TypeScript compile ES6, ES5, and Node code.

Tsconfig. json Important field

  • Files – Sets the name of the file to compile;
  • Include – Sets files to be compiled and supports path pattern matching.
  • Exclude – Sets files that do not need to be compiled and supports path pattern matching.
  • CompilerOptions – Sets the options related to the compilation process.

CompilerOptions options

{
  "compilerOptions": {
  
    /* Basic options */
    "target": "es5".// Specify the ECMAScript target version: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs".// Specify the module to use: 'commonJS ',' AMD ', 'system', 'umd' or 'es2015'
    "lib": [].// Specify the library file to be included in the build
    "allowJs": true.// Allows javascript files to be compiled
    "checkJs": true.// Report errors in javascript files
    "jsx": "preserve".// Specify JSX code generation: 'preserve', 'react-native', or 'react'
    "declaration": true.// Generate the corresponding '.d.ts' file
    "sourceMap": true.// Generate the corresponding '.map' file
    "outFile": ". /".// Merge the output files into a single file
    "outDir": ". /".// Specify the output directory
    "rootDir": ". /".// To control the output directory structure --outDir.
    "removeComments": true.// Delete all comments after compilation
    "noEmit": true.// No output file is generated
    "importHelpers": true.// Import helper functions from tslib
    "isolatedModules": true.// Treat each file as a separate module (similar to 'ts.transpilemodule').

    /* Strict type checking options */
    "strict": true.// Enable all strict type checking options
    "noImplicitAny": true.// There is an error with an implied any type on expressions and declarations
    "strictNullChecks": true.// Enable strict null checking
    "noImplicitThis": true.// An error is generated when this is of type any
    "alwaysStrict": true.// Check each module in strict mode and add 'use strict' to each file

    /* Additional checks */
    "noUnusedLocals": true.// An error is thrown when there are unused variables
    "noUnusedParameters": true.// An error is thrown if there are unused arguments
    "noImplicitReturns": true.// An error is thrown when not all code in a function returns a value
    "noFallthroughCasesInSwitch": true.// Switch statement fallthrough error reported. (that is, switch case statements are not allowed to run through)

    /* Module parsing options */
    "moduleResolution": "node".// Select a module parsing strategy: 'node' (node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": ". /".// The base directory for resolving non-relative module names
    "paths": {},                           // List of module names to baseUrl pathmaps
    "rootDirs": [].// A list of root folders whose combined contents represent the structural contents of the project runtime
    "typeRoots": [].// A list of files containing type declarations
    "types": [].// A list of type declaration filenames to include
    "allowSyntheticDefaultImports": true.// Allow default imports from modules that do not have default exports set.

    /* Source Map Options */
    "sourceRoot": ". /".// Specify where the debugger should find TypeScript files instead of source files
    "mapRoot": ". /".// Specify where the debugger should find the mapping file instead of the build file
    "inlineSourceMap": true.// Generate a single soucemaps file instead of generating different sourcemaps files
    "inlineSources": true.// Generate the code and sourcemaps in a file, requiring that either the --inlineSourceMap or --sourceMap attributes are set

    /* Other options */
    "experimentalDecorators": true.// Enable the decorator
    "emitDecoratorMetadata": true          // Support metadata for decorators}}Copy the code

More and more

Click on the link below to practice your hand

Typescript exercises

If there are any mistakes in this article, please point them out in the comments section, and click “like” if you think it will help you