1.TypeScript data types

Typescript adds type validation (type declarations) to make writing code more formal and maintainable

The base type

Typescript provides us with the following data types:

  • String type (string)
  • Number type
  • Boolean type (Boolean)
  • Null, and undefined
  • Array type (array)
  • Tuple Type
  • Enumeration Type (enum)
  • Any type (any)
  • Void type
  • Never type

Compared to JS data types, typescript has more tuple types, enumerated types, arbitrary types, void types, and never types. Of course, these are just the basic types, but there are many more types that you can learn more about later in type inference and advanced types.

Variable definitions

Write ts code variable can specify its type, after the specified type must be assigned to the specified type, otherwise an error is reported.

  • Ts type corollaries help provide types if no type is specified, see TS type corollaries.
var flag:boolean = true
flag = 123 // Error, type inconsistent
Copy the code

The data type

String type (string)

var str:string = 'this is ts';

str='haha';  / / right

// str=true; / / error
Copy the code

Number type

var num:number = 123;

num = 456; / / right

// num='str'; / / error
Copy the code

Boolean type (Boolean)

var flag:boolean = true

flag = false / / right

// flag=123; / / error
Copy the code

Null, and undefined

Undefined:

{
    // In js, a variable is declared but not initialized as undefined
    var undefinedTest:number
    // console.log(undefinedTest) // typescript error, assignment is correct

    // In typescript, declared uninitialized values need to be defined as undefined to be accessed directly
    var undefinedTest2:undefined
    console.log(undefinedTest2) // use undefined
}
{
    // May be number type may be undefined
    var undefinedTest3:number | undefined;
    console.log(num);
}
Copy the code

null:

// null is a null pointer object, undefined is an uninitialized variable. Therefore, you can think of undefined as an empty variable and NULL as an empty object
var nullTest:null
nullTest = null
// nullTest = {} // error defined type null, value must be NULL
Copy the code

Array type (array)

Ts can define arrays in two ways. First, the element type can be followed by [] to represent an array of elements of that type:

/ / the first
var arr:number[] = [1.2.3]
Copy the code

The second way is to use Array generics, Array< element type > :

/ / the second
var arr2:Array<number> = [1.2.3]
Copy the code

Tuple Type

Like arrays, elements are of different types:

let arr:[number,string] = [123.'this is ts']
Copy the code

Enumeration Type (enum)

Usage:

Enum Enumeration name {identifier [= integer constant], identifier [= integer constant],... Identifier [= integer constant],}Copy the code
enum Flag {success = 1,error = 2};

let s:Flag = Flag.success // Use values from enumerated types
console.log('Correct state',s)
let f:Flag = Flag.error
console.log('Error state',f)
Copy the code

Any type (any)

Specify a type for variables whose type is not known at programming time

var number:any = 123
number = 'str'
number = true
Copy the code

Void type

Void in typescript means that there is no type at all and is generally used to define methods that return no value.

// Indicates that the method does not return any type
function run() :void {
    console.log('run')
}

run()
Copy the code

Never type

Represents the types of values that never exist, such as exceptions

var a:never

// a = 123
a = (() = > {
    throw new Error('wrong'); }) ()Copy the code

2. The TypeScript function

Description: function definition, optional parameters, default parameters, remaining parameters, function overload, arrow function.

Definition of a function

Grammar:

// Function declaration
function fn(x: Type, y: Type) :Type {}

// Function expression
var fn = (x: Type, y: Type): Type= > {}

// Function expression: specifies the type of the variable fn
var fn: (x: Type, y: Type) = > Type = (x, y) = > {}
Copy the code
  • There are two forms of function definition: function declaration and function expression. The type can be specified by defining the parameters and return values of a function. When a function is called, the parameter types passed in must be the same as those defined for the function.
// function declaration method
function run(x: number, y: number) :number {
    return x + y;
}

// function expression method
var run2 = (x: number, y: number): string= > {
    return 'run2'
}

run(1.2);
run2(1.2);
Copy the code

In this code, the functions run and run2 specify parameter types, which must be the same when called.

  • Function expression method is another way of writing it
var run3: (x: number, y: number) = > string = function(x: number, y: number) :string{
    return 'run3';
}
run3(1.2);
Copy the code

When specifying a type for the variable run3, it should be the constraint type for the function’s parameters and return value. If we use the ts type inference we learned later, we can abbreviate it as:

var run4: (x: number, y: number) = > string = function(x, y){ // Type corollaries can determine the type of a function's argument and return value, thus omiting the type specification
    return 'run4';
}
run4(1.2);
Copy the code
  • Void specifies the return value type
function voidFnc() :void{
    console.log('Void' for methods that return no value)
}
voidFnc();
Copy the code

Optional parameters

  • In es5, method arguments and line arguments can be different, but they must be the same in TS. If they are different, you need to add? This is the optional parameter.
function electParam(name:string, age? :number) :string {
    // Age is optional
    if(age){
        return `${name} --- ${age}`
    }else{
        return `${name}-- Age is confidential}}console.log('Optional arguments', electParam('dz'))

// Note: Optional parameters must be configured at the end of the parameter

// Error: optional arguments are not at the end
// function electParam2(name? : string, age: number): string {
/ /...
// }
Copy the code

The default parameters

  • You cannot set default parameters in ES5. You can set default parameters in ES6 and TS
// age is the default parameter
function defaultParam(name:string, age:number = 20) :String {
    return `${name} --- ${age}`
}

console.log('Default parameters', defaultParam('dz'))
Copy the code

The remaining parameters

  • The three-point operator can be used when there are many arguments or the number of arguments is uncertain
The sum argument is passed as an array
function sum(. result: number[]) :number {
    var sum = 0;

    for (var i = 0; i < result.length; i++) {

        sum += result[i];
    }

    return sum;
}
console.log('Remaining parameters', sum(1.2.3.4.5.6));

// A =1, b=2
function sum2(a: number, b: number, ... result: number[]) :number {
    var sum = a * b;

    for (var i = 0; i < result.length; i++) {

        sum += result[i];
    }

    return sum;
}
console.log('Remaining argument 2', sum2(1.2.3.4.5.6));
Copy the code

Function overloading

A function with the same name that takes different parameters and performs different functions is called function overloading.

  • Method overloading in Java: An overload is when two or more functions of the same name, but with different arguments, are overloaded.
  • Overloading in typescript: The purpose of multiple functions by providing multiple function type definitions for the same function.
  • Ts overloads are written differently from Java for ES5 and ES6 compatibility.

In es5, the function with the same name overwrites the previous function, while in ts, it does not:

function overloadingFn(x: number, y: number) :number;
function overloadingFn(x: string, y: string) :string; // The format of the function is defined above, the implementation of the function is defined belowfunction overloadingFn(x: any, y: any) :any {
    return x + y;
}

overloadingFn(1.2);
overloadingFn('a'.'b');
Copy the code

In this code, the function overloadingFn of the same name defines the format of the two functions first, and then implements the function. It used to be multiple functions to pass different types of arguments. Now it can implement the function with the same name.

Arrow function

Arrow functions are the same as in ES6

setTimeout((a)= > {
    console.log('Arrow function')},1000);
Copy the code

TypeScript class

Es5 classes in the

Class creation, static methods, inheritance (object impersonation inheritance, prototype chain inheritance, object impersonation + prototype chain combination inheritance)

Es5 of object-oriented, constructor, prototype and prototype chain nature can see this document caibaojian.com/javascript-… Personally, I think it’s very clear.

1.1 Creation of classes

Es5 classes can add properties and methods to both constructors and stereotype chains, where properties are shared by multiple instances, but constructors are not.


function Person() {
    this.name = 'Ming'
    this.run = function() {
        console.log(this.name + 'In motion')
    }
}

Person.prototype.sex = 'male' // Attributes on the prototype chain are shared by multiple instances
Person.prototype.work = function() {
    console.log(this.name + 'At work')}var p = new Person()
p.run()
p.work()
console.log(p.name)

Copy the code

1.2 Static Methods

Calling static methods does not require instantiation


Person.getInfo=function(){
    console.log('I'm a static method');
}
Person.getInfo();

Copy the code

1.3 Implementation Inheritance

Object impersonation (or constructor inheritance) inheritance: You can inherit properties and methods from constructors, but not properties and methods from the stereotype chain

Stereotype inheritance: Attributes and methods can be inherited from constructors, as well as from properties and methods above the stereotype chain, but cannot be passed to the parent class when instantiating a child class

The following is a combination of object impersonation + prototype chain inheritance to solve the problems of the above two inheritance methods


function Worker(name,age){
    this.name=name;  * / / * attributes
    this.age=age;
    this.run=function(){  /* Instance method */
        alert(this.name+'In motion');
    }

}      
Worker.prototype.sex="Male";
Worker.prototype.work=function(){
    alert(this.name+'At work');
}
    
function Web(name,age){
    Worker.call(this,name,age);  // Objects can inherit attributes and methods from constructors. Instantiated children can pass arguments to their parent classes
}
// Web.prototype = new Worker(); // Prototype chain inheritance method 1: Inherit the Worker constructor and all the methods and attributes on the prototype
Web.prototype = Worker.prototype;  / / prototype chain inheritance method 2: the optimization method is a repeat inherited constructor properties and methods of problem (nature can take a look at http://caibaojian.com/javascript-object-5.html)

var w = new Web('zhao four'.20);   
w.run();
w.work();

Copy the code

As can be seen from the above, object impersonation inheritance is to inherit the attributes and methods of the parent class Worker’s constructor through the call method in the subclass Web constructor. The inheritance of prototype chain is realized by the prototype object of subclass Web equal to the prototype object of parent class Worker. Finally, the combination of the two inheritance methods achieves perfect inheritance.

3. The class in the typescript

Content overview: TS class definition, inheritance, class modifiers, static properties and static methods, polymorphism, abstract classes and abstract methods

2.1 Definition of classes in TS

The definition of a class in TS is the same as that of an ES6 class


class PersonDefine {
    name: string // attribute, with the public keyword omitted
    constructor(name:string) { // constructor
        this.name = name
    }
    run():string { / / prototype
        return `The ${this.name}In the sports `}}var define = new PersonDefine('Class definition')
alert(define.run())

Copy the code

2.2 inheritance

Ts inheritance is much simpler than ES5 and is implemented using extends Super


class WebExtend extends PersonDefine {
    constructor(name:string) {
        super(name) // super inherits the superclass constructor and passes arguments to the superclass constructor
    }
    work():string {
        return `The ${this.name}In the work `}}var extend = new WebExtend('inherit')
alert(extend.run())
alert(extend.work())

Copy the code

2.3 Ts modifiers

Modifiers: typescript provides three types of modifiers when defining properties

  • Public: A public modifier that can be accessed inside, outside, or in subclasses of the current class
  • Protected: Protected type, accessible within the current class, within subclasses, but not outside the class
  • Private: Private modifier, accessible within the current class, but not by subclasses or outside the class

Note: Attributes without modifiers default to public modifiers


// Take private for example
class PersonPrivate{
    private name:string;  /* Private attributes => Private attributes */
    constructor(name:string){
        this.name=name;
    }
    run():string{
        return `The ${this.name}In the sports ` // Private attributes can only be accessed within the current class}}class Web extends PersonPrivate{
    constructor(name:string){
        super(name)
    }
    work(){
        // return '${this.name} is working' // error: subclass cannot access private attributes of its parent class}}var privateName = new PersonPrivate('private')
alert(privateName.run())
// console.log(privateName. Name) // Error, external cannot access the private properties of the class

Copy the code

2.4 Static properties and methods

Why static properties and methods? The $. Ajax method in JQ is static


function $(element) {
    return new Base(element)
}

function Base(element) {
    this.element = document.getElementById(element)
    this.css = function(arr, value) {
        this.element.style[arr] = value
    }
}
$('box').css('color'.'red')
$.ajax = function() {}  // What if you want to use methods on $, static methods

Copy the code

Ts static properties and static methods use static


class PersonStatic{
    /* Public attributes */
    public name:string;
    constructor(name:string) {
        this.name=name;
    }
    /* Instance method (needs to be instantiated, so instance method) */
    run(){  
        return `The ${this.name}In the sports `
    }
    /* Static attributes */
    static sex = 'male'
    /* static method, there is no way to call the class property */ directly
    static info(){  
        // return 'info method '+ this.name // Static methods cannot call methods and properties of this class. Static properties can be called
        return Method of 'info' + PersonStatic.sex
    }
}

console.log('Static method' + PersonStatic.info())
console.log('Static properties' + PersonStatic.sex)

Copy the code

2.5 polymorphic

A parent class defines a method that is not implemented and lets its subclasses implement it, each of which behaves differently

  • Polymorphism is inheritance

For example, if we define a parent class of Animal, we will not implement the eat method, and let the subclasses Dog and Cat implement their own eat methods


class Animal {
    name:string;
    constructor(name:string) {
        this.name=name;
    }
    eat(){   The // eat method inherits its subclasses to implement}}class Dog extends Animal{
    constructor(name:string){
        super(name)
    }
    eat(){
        return this.name+'Eat grain'}}class Cat extends Animal{
    constructor(name:string){
        super(name)
    }
    eat(){
        return this.name+'Eat a mouse'}}Copy the code

2.6 Abstract Classes and methods

Definition: The abstract keyword is used to define abstract classes and abstract methods. Abstract methods in an abstract class contain no concrete implementation and must be implemented in a derived class (a subclass of the abstract class)

  • Abstract class: It is a base class that provides inheritance from other classes. It cannot be instantiated directly. Subclass inheritance can be instantiated
  • Methods modified by abstract (abstract methods) can only be placed in abstract classes
  • Abstract classes and methods are used to define the standard (for example, if the abstract class Animal has the abstract eat method, its subclasses must contain the eat method).

abstract class AnimalAbst{
    public name:string;
    constructor(name:string){
        this.name=name;
    }
    abstract eat():any;  // Abstract methods do not contain concrete implementations and must be implemented in derived classes
    run(){
        console.log('Other methods may not be implemented.')}}// var a = new Animal() /*

class DogAbst extends Animal{
    // Subclasses of the abstract class must implement the abstract methods in the abstract class
    constructor(name:any){
        super(name)
    }
    eat(){
        return this.name + 'Eat grain'}}var d = new DogAbst('Little Flower');
console.log('Abstract Classes and Methods',d.eat());

Copy the code

4. TypesSript interface

Interface definition: The interface is to restrict the incoming parameters; Or to declare and constrain the properties and methods in the class, the class implementing the interface must implement the properties and methods in the interface; Interfaces in typescript are defined by the interface keyword.

Interface functions: The interface defines the specification that a certain group of classes need to comply with. The interface does not care about the internal state data of these classes, nor the implementation details of the methods in these classes. It only stipulates that certain methods must be provided in this group of classes, and the classes that provide these methods can meet the actual needs. Interfaces in typescrip are similar to Java, with more flexible interface types including properties, functions, indexable, and classes.

Description: Interface classification (attribute interface, function type interface, indexable interface, class type interface), interface inheritance

1. Interface classification

1.1 Attribute Interface

Constraints on incoming objects (that is, constraints on JSON)

Before we get to the interface, let’s look at the function passing in the obj argument


function printLabel(labelInfo: {label:string}){
    return labelInfo
}
// printLabel({name:'obj'}); // This is not the case
console.log(printLabel({label: 'obj'}))

Copy the code

Similarly, the attribute interface => is introduced to constrain the method’s incoming parameters

The following is an example of an attribute interface, where the printFullName method constrains the FullName(which is an object) passed in


interface FullName{
    firstName: string; / / note; The end of thesecondName: string; age? : number// The optional attributes of the interface use?
}

function printFullName(name:FullName) {
    // The passed object must contain firstName and secondName, with or without age
    return name
}
var obj = {
    firstName:'small'.secondName:'Ming'.age: 20
}
console.log(printFullName(obj))

Copy the code

Attribute interface application: Native JS encapsulates Ajax


interface Config{
    type: string; url: string; data? : string; dataType: string; }function ajax(config: Config) {
    var xhr = new XMLHttpRequest
    xhr.open(config.type, config.url, true)
    xhr.send(config.data)
    xhr.onreadystatechange = function() {
        if(xhr.readyState == 4 && xhr.status == 200) {
            if(config.dataType == 'json') {console.log(JSON.parse(xhr.responseText))
            }else{
                console.log(xhr.responseText)
            }
        }
    }
}

ajax({
    type: 'get'.data: 'name=xiaoming'.url: 'http://a.itying.com/api/productlist'.dataType: 'json'
})

Copy the code

1.2 Function Type Interface

Constrain the parameters passed to the method as well as the return values


interface encrypt{
    (key: string, value: string): string; // The type of the argument passed in and the return value
}

var md5:encrypt = function(key:string, value:string) :string{
    // encrypt constrains the md5 encryption method, and the parameter and return value types of the MD5 method must be the same as those of encrypt
    return key + value
}

console.log(md5('name'.'Ming'))

Copy the code

1.3 Indexable Interface

Constraints on indexes and incoming parameters (commonly used for constraints on arrays and objects)

Ts defines an array:


var arr1:number[] = [1.2]
var arr2:Array<string> = ['1'.'2']

Copy the code

Now use the interface to implement:


// Constraints on arrays
interface UserArr{
    // The index is number and the parameter is string
    [index:number]: string
}
var userarr:UserArr = ['a'.'b']
console.log(userarr)

Copy the code

// Constraints on objects
interface UserObj{
    // The index is string and the argument is string
    [index:string]: string
}
var userobj:UserObj = { name: 'Ming'.sex: 'male' }
console.log(userobj)

Copy the code

1.4 Class type interface

Constraints on classes are similar to abstract class abstractions


interface Animal{
    // Restrict the attributes and methods in the class
    name:string;
    eat(str:string):void;
}
// To implement an interface with the implements keyword, you must implement methods and properties declared in the interface
class Cat implements Animal{
    name:string;
    constructor(name:string){
        this.name = name
    }
    eat(food:string){
        console.log(this.name + 'eat' + food)
    }
}
var cat = new Cat('flower')
cat.eat('mouse')

Copy the code

2. Interface inheritance

Like class inheritance, extends implements interface inheritance

The following implements both class inheritance and interface inheritance


interface Animal {
    eat(): void;
}
// The class that implements the Person interface must also implement the Animal methods
interface Person extends Animal {
    work(): void;
}

class Programmer {
    public name: string;
    constructor(name: string) {
        this.name = name;
    }
    coding(code: string) {
        console.log(this.name + code)
    }
}

// Inherits classes and implements interfaces
class Web extends Programmer implements Person {
    constructor(name: string) {
        super(name)
    }
    eat() {
        console.log(this.name + 'eat')
    }
    work() {
        console.log(this.name + 'work'); }}var w = new Web('xiao li');
w.eat();
w.coding('Write TS code');

Copy the code

5. TypesSript generics

Generics: Most of the time, types are written dead and not reusable. Generics can be simply understood as setting variables for values like types, addressing the reuse of classes, interfaces, methods, and support for non-specific data types.

  • Syntax: < type variable name >, the general system uses single-letter uppercase, such as T, S…

Overview: Generic functions, generic classes, generic interfaces

Generic function

The type of parameter passed in and the type of parameter returned can be specified

Let’s look at a function that uses the TS data type and wants to return both string and number


function getData1(value:string) :string{
    return value;
}
function getData2(value:number) :number{
    return value;
}

Copy the code

In this way, we need to write different functions and cannot return different types of data as required, resulting in code redundancy => Thus introducing generics

Represents a generic type that is called to specify the data type of T


function dataT<T> (value:T) :T{
    // The return value is T
    return value
}
dataT<number>(1) // If the call specifies a generic type of number, the parameter passed must also be of type number
dataT<string>('string')

function dataAny<T> (value:T) :any{
    return 'Pass in parameter T, return value of any type';
}
dataAny<number>(123); // The parameter must be number
dataAny<string>('This is a generic.');

Copy the code

A generic class

Generic classes use (<>) to enclose generic types after the class name.

There is a minimum heap algorithm that needs to support returning both numbers and strings

Before generics: you could only specify data types in the class part of a class, and the implementation required writing a string class


class MinClass{
    public list:number[]=[];
    add(num:number){
        this.list.push(num)
    }
    min():number{
        var minNum=this.list[0];
        for(var i=0; i<this.list.length; i++){if(minNum>this.list[i]){
                minNum=this.list[i]; }}returnminNum; }}var m=new MinClass();
m.add(1);
m.add(2);
alert(m.min());

Copy the code

After using generics: Use only one set of classes to implement


class MinClassT<T>{
    public list:T[]=[];
    add(value:T):void{
        this.list.push(value);
    }
    min():T{        
        var minNum=this.list[0];
        for(var i=0; i<this.list.length; i++){if(minNum>this.list[i]){
                minNum=this.list[i]; }}returnminNum; }}var m1=new MinClassT<number>();   /* Instantiates the class and specifies that the T of the class represents the type number*/
m.add(1);
m.add(2);
alert(m1.min())

var m2=new MinClassT<string>();   /* Instantiates the class and specifies that T of the class represents type string*/
m2.add('c');
m2.add('a');
alert(m2.min())

Copy the code

A generic interface

There is a function type interface


interface ConfigFn{
    (value:string):string;
}
var setData:ConfigFn = function(value:string) :string{
    return value
}
setData('name');
// setData(20); / / error

Copy the code

setData(20); If you want to pass in a parameter of type number, you need to write a function type interface => use a generic interface

Generic interfaces are written in two ways:


// The generic interface is defined in mode 1
interface ConfigFnOne{
    <T>(value:T):T;
}
var setDataOne:ConfigFnOne = function<T> (value:T) :T{
    return value
}
// We can pass both string and number arguments
setDataOne<string>('name');
setDataOne<number>(20);

Copy the code

// The generic interface is defined in mode two
interface ConfigFnTwo<T>{
    (value:T):T;
}
function setDataTwo<T> (value:T) :T{
    return value
}
var setDataTwoFn:ConfigFnTwo<string> = setDataTwo
setDataTwoFn('name');

Copy the code

6.TypesSript

Sometimes it is not necessary to enforce type declarations, and in some cases ts type corollaries help provide types where they are not explicitly specified.

Description of content: variable initialization type inference, context type inference.

Variable initialization

Ts does type inference based on the value assigned to the variable when it is initialized.

let a = 'Type inference';
// a = true; // Type 'true' is not assignable to type 'string'
Copy the code

In the code above, a does not specify a type. Ts will infer that a is of type string. When a = true is reassigned, the type mismatch will be reported, and the vscode compiler will raise an error.

Context inference

Ts also makes type inferences based on context, such as in event functions, where the first parameter of the function inferences the processing event object based on the currently bound event type.

document.onkeydown = function(e) {
    // console.log(e.button); //<- Error Property 'button' does not exist on type 'KeyboardEvent'
};
Copy the code

This example will result in a type error. The ts type checker will automatically infer that e is of type KeyboardEvent based on the current binding event class onKeyDown.

TypesSript Advanced type

Intersection Types

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. For example, Person & Serializable & Loggable are both Person and Serializable and Loggable. This means that an object of this type has members of all three types.

We mostly see the use of cross types in mixins or other places that don’t fit into a typical object-oriented model. (There are plenty of occasions when this happens in JavaScript!) Here’s a simple example of how to create mixins (“target”: “ES5 “) :

function extend<T.U> (first: T, second: U) :T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if(! result.hasOwnProperty(id)) { (<any>result)[id] = (<any>second)[id]; }}return result;
}

class Person {
    constructor(public name: string) {}}interface Loggable {
    log(): void;
}
class ConsoleLogger implements Loggable {
    log() {
        // ...}}var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
Copy the code

Union Types

Union types are closely related to cross types, but their usage is completely different. Occasionally you’ll run into a code base that wants to pass in a number or string argument. For example, the following function:

/** * Takes a string and adds "padding" to the left. * If 'padding' is a string, then 'padding' is appended to the left side. * If 'padding' is a number, then that number of spaces is added to the left side. */
function padLeft(value: string, padding: any) {
    if (typeof padding === "number") {
        return Array(padding + 1).join("") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'. `);
}

padLeft("Hello world".4); // returns " Hello world"
Copy the code

A problem with padLeft is that the padding parameter is of type any. This means that we can pass in an argument that is neither number nor string, but TypeScript does not report an error.

let indentedString = padLeft("Hello world".true); // Compile phase passed, run error
Copy the code

In a traditional object-oriented language, we might abstract these two types into hierarchical types. This is clearly clear, but there is also over-design. One of the benefits of the original version of padLeft is that it allows us to pass in primitive types. It’s easy and convenient to use. This new approach won’t work if we just want to use existing functions.

Instead of any, we can use the union type as the padding argument:

/** * Takes a string and adds "padding" to the left. * If 'padding' is a string, then 'padding' is appended to the left side. * If 'padding' is a number, then that number of spaces is added to the left side. */
function padLeft(value: string, padding: string | number) {
    // ...
}

let indentedString = padLeft("Hello world".true); // errors during compilation
Copy the code

Union types indicate that a value can be one of several types. We use a vertical bar (|) separated for each type, so the number | string | Boolean said a value can be a number, a string, or Boolean.

If a value is a union type, we can access only the members that are common to all types of that union type.

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet() :Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors
Copy the code

The union types here can be a bit complicated, but you can easily get used to them. If A value is of type A | B, we can sure of is that it contains A and B some members of the communist party of China. In this example, Bird has a fly member. We are not sure a Bird if there is a fly way | Fish types of variables. If the variable is of type Fish at run time, the call to pet.fly() is an error.

Type Guards and Differentiating Types

Union types are suitable for cases where values can be of different types. But what happens when we want to know for sure if it’s Fish? A common way to distinguish between two possible values in JavaScript is to check if the member exists. As mentioned earlier, we can only access co-owned members of the union type.

let pet = getSmallPet();

// Every member accesses an error
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}
Copy the code

To make this code work, we use type assertions:

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}
Copy the code

User – defined type guard

Notice here that we have to use type assertions multiple times. It would be nice if once we checked the type, we could clearly know the type of PET in each subsequent branch.

TypeScript’s type-guarding makes this a reality. Type guards are expressions that are checked at run time to make sure the type is in a scope. To define a type guard, we simply define a function whose return value is a type predicate:

function isFish(pet: Fish | Bird) :pet is Fish {
    return(<Fish>pet).swim ! = =undefined;
}
Copy the code

In this case, pet is Fish is the type predicate. The predicate is of the form parameterName is Type. ParameterName must be a parameterName from the current function signature.

Whenever isFish is called with some variable, TypeScript reduces the variable to that specific type, as long as the type is compatible with the variable’s original type.

// The 'swim' and 'fly' calls are ok now

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}
Copy the code

Note that TypeScript not only knows that pet is Fish in the if branch; It also knows that in the else branch, it must not be Fish, it must be Bird.

typeofType the guards

Now let’s go back and see how to write padLeft code using union types. We can use type assertions as follows:

function isNumber(x: any) :x is number {
    return typeof x === "number";
}

function isString(x: any) :x is string {
    return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join("") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'. `);
}
Copy the code

However, it is painful to have to define a function to determine whether a type is primitive. Fortunately, we don’t have to abstract typeof x === “number” as a function right now, because TypeScript recognizes it as a type guard. That means we can check the type directly in the code.

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join("") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'. `);
}
Copy the code

Only two forms of these *typeof type guards * can be recognized: typeof V === “typename” and typeof V! “Typename”, “typename” must be “number”, “string”, “Boolean” or “symbol”. But TypeScript doesn’t prevent you from comparing with other strings, and the language doesn’t recognize those expressions as type guards.

instanceofType the guards

If you’ve read typeof type guards and are familiar with the instanceof operator in JavaScript, you’ve probably already guessed what this section is about.

Instanceof type guard is a way to refine types through constructors. For example, let’s take a look at the previous string padding example:

interface Padder {
    getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(""); }}class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value; }}function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("");
}

/ / type for SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // Type refined to 'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // Type refinement to 'StringPadder'
}
Copy the code

The right side of instanceof is required to be a constructor, which TypeScript refines to:

  1. Of this constructorprototypeProperty if its type is notanyif
  2. Constructs the union of types returned by the signature

In that order.

Can I do fornullThe type of

TypeScript has two special types, null and undefined, which have values null and undefined, respectively. We have explained this briefly in the basic types section. By default, the type checker assumes that null and undefined can be assigned to any type. Null and undefined are valid values for all other types. This also means that you can’t prevent them from being assigned to other types, even if you wanted to. Null’s inventor, Tony Hoare, calls it a billion-dollar mistake.

StrictNullChecks the strictNullChecks flag solves this problem: when you declare a variable, it does not automatically contain null or undefined. You can explicitly include them using union types:

let s = "foo";
s = null; // error, 'null' cannot be assigned to 'string'
let sn: string | null = "bar";
sn = null; / / can

sn = undefined; / / the error, "undefined" cannot be assigned to the 'string | null'
Copy the code

Note that TypeScript treats null and undefined differently according to JavaScript semantics. String | null string | undefined and string | undefined | null is a different type.

Optional parameters and optional properties

Use – strictNullChecks, optional parameters will be automatically add | undefined:

function f(x: number, y? :number) {
    return x + (y || 0);
}
f(1.2);
f(1);
f(1.undefined);
f(1.null); // error, 'null' is not assignable to 'number | undefined'
Copy the code

The same is true for optional attributes:

class C {
    a: number; b? :number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'
Copy the code

Type guard and type assertion

Since null-capable types are implemented by union types, you need to use type guards to remove nulls. Fortunately this is consistent with code written in JavaScript:

function f(sn: string | null) :string {
    if (sn == null) {
        return "default";
    }
    else {
        returnsn; }}Copy the code

Here null is obviously removed, and you can also use the short-circuit operator:

function f(sn: string | null) :string {
    return sn || "default";
}
Copy the code

If the compiler is unable to remove null or undefined, you can do so manually using type assertions. Syntax is added! The suffix: identifier. Remove null and undefined from the identifier type:

function broken(name: string | null) :string {
  function postfix(epithet: string) {
    return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null
  }
  name = name || "Bob";
  return postfix("great");
}

function fixed(name: string | null) :string {
  function postfix(epithet: string) {
    returnname! .charAt(0) + '. the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}
Copy the code

This example uses nested functions because the compiler cannot remove null from nested functions (except for function expressions that are called immediately). Because it can’t keep track of all calls to nested functions, especially if you use the inner function as the return value of the outer function. If you don’t know where the function is called, you don’t know the type of name at the time of the call.

Type the alias

A type alias gives a type a new name. Type aliases are sometimes similar to interfaces, but can work with primitive values, union types, tuples, and any other type you need to write by hand.

type Name = string;
type NameResolver = (a)= > string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver) :Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        returnn(); }}Copy the code

Aliasing does not create a new type – it creates a new name to refer to that type. Aliasing primitive types is usually useless, although it can be used as a form of documentation.

Like interfaces, type aliases can also be generic – we can add type parameters and pass them to the right of the alias declaration:

type Container<T> = { value: T };
Copy the code

We can also use type aliases to refer to ourselves in attributes:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}
Copy the code

With cross types, we can create some pretty bizarre types.

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
Copy the code

However, the type alias cannot appear anywhere on the right side of the declaration.

type Yikes = Array<Yikes>; // error
Copy the code

Interfaces vs. type aliases

As we mentioned, type aliases can act like interfaces; However, there are some nuances.

First, the interface creates a new name that can be used anywhere else. Type aliases do not create new names – for example, they are not used for error messages. In the following example code, hovering over interfaced in the compiler shows that it returns Interface, but hovering over Aliased shows the object literal type.

type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias) :Alias;
declare function interfaced(arg: Interface) :Interface;
Copy the code

Another important difference is that type aliases cannot extends and implements (nor themselves can extends and implements other types). Because objects in software should be open to extension but closed to modification, you should try to use interfaces instead of type aliases.

On the other hand, if you cannot describe a type through an interface and need to use a union or tuple type, type aliases are often used.

String literal type

The string literal type allows you to specify a fixed value that a string must have. In practice, string literal types work well with union types, type guards, and type aliases. By combining these features, you can implement strings that resemble enumerations.

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {}else if (easing === "ease-in-out") {}else {
            // error! should not pass null or undefined.}}}let button = new UIElement();
button.animate(0.0."ease-in");
button.animate(0.0."uneasy"); // error: "uneasy" is not allowed here
Copy the code

You can only pass one of the three allowed characters as an argument, passing any other value will generate an error.

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'
Copy the code

String literals can also be used to distinguish function overloading:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}
Copy the code

Numeric literal type

TypeScript also has numeric literal types.

function rollDie() : 1 | 2 | 3 | 4 5 | | 6{
    // ...
}
Copy the code

We rarely use them directly, but they can be used to narrow down the scope of bug debugging:

function foo(x: number) {
    if(x ! = =1|| x ! = =2) {
        / / ~ ~ ~ ~ ~ ~ ~
        // Operator '! ==' cannot be applied to types '1' and '2'.}}Copy the code

In other words, when x is compared to 2, its value must be 1, which means that the comparison check above is illegal.

Enumerator type

As we mentioned in the enumeration section, enumerators are typed when each enumerator is initialized with a literal.

When we talk about “singletons,” we mostly mean enumerator types and number/string literals, although most users use “singletons” and “literals” interchangeably.

Discriminated Unions

You can combine singletons, union types, type guards, and type aliases to create a high-level pattern called recognizable union, also known as label union or algebraic data types. Discernible unions are useful in functional programming. Some languages automatically recognize associations for you; TypeScript is based on existing JavaScript patterns. It has three elements:

  1. With ordinary singleton type attributes —Recognizable features.
  2. A type alias contains the union of those types —The joint.
  3. The type guard on this property.
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
Copy the code

First we declare the interfaces to be federated. Each interface has a kind attribute but has a different string literal type. A kind attribute is called an identifiable feature or tag. Other attributes are specific to each interface. Note that there is currently no connection between the interfaces. Let’s put them together:

type Shape = Square | Rectangle | Circle;
Copy the code

Now we use recognizable union:

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2; }}Copy the code

Integrity check

We want the compiler to be able to notify us when not all recognizable union changes are covered. For example, if we added Triangle to Shape, we would also need to update the area:

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
    // should error here - we didn't handle case "triangle"
}
Copy the code

There are two ways to do this. The first is to enable –strictNullChecks and specify a return value type:

function area(s: Shape) :number { // error: returns number | undefined
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2; }}Copy the code

Because switch doesn’t include all cases, TypeScript assumes that this function will sometimes return undefined. If you explicitly specify the return value type for the number, then you will see an error, because actually return a value of type number | is undefined. However, there are some subtleties to this approach and strictNullChecks does not support old code very well.

The second method uses the never type, which the compiler uses for completeness checks:

function assertNever(x: never) :never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // error here if there are missing cases}}Copy the code

Here, assertNever checks whether S is of type never — that is, the type that remains after all possible cases have been removed. If you forget a case, s will have a true type and you will get an error. This approach requires you to define an extra function, but it’s also more obvious when you forget a case.

Polymorphism of thethistype

The polymorphic this type represents a subtype that contains a class or interface. This is called F-bounded polymorphism. It can easily represent inheritance between coherent interfaces, such as. In the calculator example, we return this after each operation:

class BasicCalculator {
    public constructor(protected value: number = 0) {}public currentValue(): number {
        return this.value;
    }
    public add(operand: number) :this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number) :this {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}

let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();
Copy the code

Since this class uses the this type, you can inherit it, and the new class can use the previous method without making any changes.

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... other operations go here ...
}

let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue();
Copy the code

Without this type, ScientificCalculator would not be able to inherit from BasicCalculator while maintaining interface continuity. Multiply will return the BasicCalculator, which has no sin method. However, with this type, multiply returns this, which in this case is ScientificCalculator.

Index types

Using index types, the compiler can examine code that uses dynamic attribute names. For example, a common JavaScript pattern is to pick a subset of properties from an object.

function pluck(o, names) {
    return names.map(n= > o[n]);
}
Copy the code

Here’s how to use this function in TypeScript with index type queries and index access operators:

function pluck<T.K extends keyof T> (o: T, names: K[]) :T[K] []{
  return names.map(n= > o[n]);
}

interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Jarid',
    age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]
Copy the code

The compiler checks to see if name is really an attribute of Person. This example also introduces several new type operators. The first is keyof T, the index type query operator. For any type T, the result of keyof T is the union of known public attribute names on T. Such as:

let personProps: keyof Person; // 'name' | 'age'
Copy the code

Keyof Person is can completely with the ‘name’ | ‘age’ replace each other. Different is that if you add other attributes to the Person, such as address: string, then keyof Person will automatically become a ‘name’ | ‘age’ | ‘address’. You can use keyof in a context like the pluck function because you don’t know the property name that might appear before you use it. But the compiler will check that you passed the correct property name to Pluck:

pluck(person, ['age'.'unknown']); // error, 'unknown' is not in 'name' | 'age'
Copy the code

The second operator is T[K], the index access operator. In this case, type syntax mirrors expression syntax. This means that person[‘name’] has type person[‘name’] – in our case, string. However, just like index-type queries, you can use T[K] in a normal context, and that’s where its power lies. You just have to make sure that the type variable K extends Keyof T. For example, the getProperty function looks like this:

function getProperty<T.K extends keyof T> (o: T, name: K) :T[K] {
    return o[name]; // o[name] is of type T[K]
}
Copy the code

GetProperty o: T and name: K mean o[name]: T[K]. When you return the result T[K], the compiler instantiates the true type of the key, so the return type of getProperty changes with the property you want.

let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'
Copy the code

Index type and string index signature

Keyof and T[K] interact with string index signatures. If you had a type with a string index signature, keyof T would be string. And T[string] is the type of the index signature:

interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number> ['foo']; // number
Copy the code

Mapping type

A common task is to make each attribute of a known type optional:

interfacePersonPartial { name? :string; age? :number;
}
Copy the code

Or we want a read-only version:

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

As is often the case in JavaScript, TypeScript provides a way to create new types from old ones – mapping types. In a mapping type, the new type converts every attribute of the old type in the same way. For example, you can make each attribute of type Readonly or optional. Here are some examples:

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

Use it like this:

type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
Copy the code

Here’s a look at the simplest mapping type and its components:

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
Copy the code

Its syntax is the same as the syntax type of the index signature, internally using for.. The in. It has three parts:

  1. Type variableK, which is bound to each property in turn.
  2. String literal associativeKeys, which contains a collection of property names to iterate over.
  3. The result type of the property.

In a simple example, Keys is a hard-coded list of attribute names and the attribute type is always Boolean, so the mapping type is equivalent to:

type Flags = {
    option1: boolean;
    option2: boolean;
}
Copy the code

In a real application, this might be different from Readonly or Partial above. They convert fields in a certain way, based on some existing type. This is what keyof and index access types do:

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P inkeyof Person]? : Person[P] }Copy the code

But it would be more useful if there were some generic versions.

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P inkeyof T]? : T[P] }Copy the code

In these cases, the property list is keyof T and the result type is a variant of T[P]. This is a good template for using generic mapping types. Because this type of transformation is homomorphic, the mapping only works on the properties of T and nothing else. The compiler knows that it can copy all existing attribute modifiers before adding any new attributes. For example, if person.name is read-only, Partial . Name will also be read-only and optional.

Here is another example where T[P] is wrapped in a Proxy

class:

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T> (o: T) :Proxify<T> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);
Copy the code

Note that Readonly

and Partial

are useful, so they’re included in the TypeScript standard library along with Pick and Record:

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

Readonly, Partial, and Pick are homomorphic, but Record is not. Because Record does not require an input type to copy properties, it is not a homomorphism:

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3'.string>
Copy the code

Non-homomorphic types essentially create new properties, so they do not copy property modifiers from them.

Inferred from the mapping type

Now that you know how to wrap a type’s properties, it’s time to unpack them. It’s actually pretty easy:

function unproxify<T> (t: Proxify<T>) :T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}

let originalProps = unproxify(proxyProps);
Copy the code

Note that this unpacking inference applies only to homomorphic mapping types. If the mapping type is not homomorphic, you need to give the unpacking function an explicit type parameter.

Conditional type

TypeScript 2.8 introduces conditional types, which can represent non-uniform types. Conditional types are tested for type relationships with a conditional expression to choose between two types:

T extends U ? X : Y
Copy the code

The above type means that the type is X if T can be assigned to U, and Y otherwise.

The conditional type T extends U? X: Y is either resolved to X, or to Y, or deferred resolution because it may depend on one or more type variables. If T or U contains type parameters, the resolution to X or Y or deferred depends on whether the type system has enough information to determine that T can always be assigned to U.

Here are some examples of types that can be resolved immediately:

declare function f<T extends boolean> (x: T) :T extends true ? string : number;

// Type is 'string | number
let x = f(Math.random() < 0.5)

Copy the code

Another example involves the TypeName alias, which uses nested conditional types:

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"
type T2 = TypeName<true>;  // "boolean"
type T3 = TypeName<(a)= > void>;  // "function"
type T4 = TypeName<string[] >;// "object"
Copy the code

Here is an example of a conditional type whose resolution is deferred:

interface Foo {
    propA: boolean;
    propB: boolean;
}

declare function f<T> (x: T) :T extends Foo ? string : number;

function foo<U> (x: U) {
    // Has type 'U extends Foo ? string : number'
    let a = f(x);

    // This assignment is allowed though!
    let b: string | number = a;
}
Copy the code

Here, the A variable contains an undetermined conditional type. When another piece of code calls foo, it replaces U with another type, and TypeScript recalculates the conditional type to determine if it can pick a branch.

At the same time, we can assign conditional types to other types as long as each branch of the conditional type can be assigned to the target type. So in our example, we can extend U extends Foo? String: the number assigned to a string | number, because no matter what the end result is that the conditional type, it can be a string or number.

Distributed conditional types

If the type to be examined within a conditional type is naked Type parameter, it is also called a “distributed conditional type.” Distributed conditional types are automatically distributed to federated types when instantiated. For example, instantiate T extends U? X, Y, T type | A | B C, will be resolved as (A extends U? X : Y) | (B extends U ? X : Y) | (C extends U ? X, Y).

example

type T10 = TypeName<string | (() = >void) >; / /"string"|"function"
type T12 = TypeName<string | string[] | undefined>;  // "string"|"object"|"undefined"
type T11 = TypeName<string[] | number[] >; / /"object"
Copy the code

T extends U? In an instantiation of X: Y, a reference to T is resolved as part of the union type (i.e., T refers to a single part after the conditional type is distributed to the union type). In addition, references to T in X have an additional type parameter constraint U (for example, T is treated as if it were assignable to U in X).

example

type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[]? BoxedArray<T[number]> : BoxedValue<T>;

type T20 = Boxed<string>;  // BoxedValue<string>;
type T21 = Boxed<number[] >;// BoxedArray<number>;
type T22 = Boxed<string | number[] >;// BoxedValue<string> | BoxedArray<number>;
Copy the code

Note that in the true branch of Boxed

, T has an additional constraint any[], so it applies to the T[number] array element type. Also notice how conditional types are distributed into union types.

The distributed attributes of conditional types can be easily used to filter union types:

type Diff<T, U> = T extends U ? never : T;  // Remove types from T that are assignable to U
type Filter<T, U> = T extends U ? T : never;  // Remove types from T that are not assignable to U

type T30 = Diff<"a" | "b" | "c" | "d"."a" | "c" | "f">;  // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d"."a" | "c" | "f">;  // "a" | "c"
type T32 = Diff<string | number | (() = >void), Function>;  // string | number
type T33 = Filter<string | number | (() = >void), Function>;  // (a)= > void

type NonNullable<T> = Diff<T, null | undefined>;  // Remove null and undefined from T

type T34 = NonNullable<string | number | undefined>;  // string | number
type T35 = NonNullable<string | string[] | null | undefined>;  // string | string[]

function f1<T> (x: T, y: NonNullable<T>) {
    x = y;  // Ok
    y = x;  // Error
}

function f2<T extends string | undefined> (x: T, y: NonNullable<T>) {
    x = y;  // Ok
    y = x;  // Error
    let s1: string = x;  // Error
    let s2: string = y;  // Ok
}
Copy the code

Conditional types are particularly useful when combined with mapping types:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

interface Part {
    id: number;
    name: string;
    subparts: Part[];
    updatePart(newName: string) :void;
}

type T40 = FunctionPropertyNames<Part>;  // "updatePart"
type T41 = NonFunctionPropertyNames<Part>;  // "id" | "name" | "subparts"
type T42 = FunctionProperties<Part>;  // { updatePart(newName: string): void }
type T43 = NonFunctionProperties<Part>;  // { id: number, name: string, subparts: Part[] }
Copy the code

Like union and cross types, conditional types do not allow recursive references to themselves. Such as the following error.

example

type ElementType<T> = T extends any[]? ElementType<T[number]> : T;  // Error
Copy the code

Type inference in conditional types

Now in the extends substatement of conditional types, you allow a infer statement, which introduces a type variable to be inferred. This inferred type variable can be referenced in the true branch of the conditional type. Infer allows multiple variables of the same type to occur.

For example, the following code extracts the return value type of the function type:

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

Conditional types can be nested to form a series of matching patterns, evaluated in order:

type Unpacked<T> =
    T extends (infer U)[] ? U :
    T extends(... args:any[]) => infer U ? U :
    T extends Promise<infer U> ? U :
    T;

type T0 = Unpacked<string>;  // string
type T1 = Unpacked<string[] >;// string
type T2 = Unpacked<(a)= > string>;  // string
type T3 = Unpacked<Promise<string> >;// string
type T4 = Unpacked<Promise<string> > [];// Promise<string>
type T5 = Unpacked<Unpacked<Promise<string> [] > >;// string
Copy the code

The following example explains how multiple candidate types of the same type variable can be inferred as joint types in a covariant position:

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string} >.// string
type T11 = Foo<{ a: string, b: number} >.// string | number
Copy the code

Similarly, in mutable positions, multiple candidate types of the same type variable are inferred to be intersecting types:

type Bar<T> = T extends { a: (x: infer U) = > void, b: (x: infer U) = > void}? U : never;type T20 = Bar<{ a: (x: string) = > void, b: (x: string) = > void} >.// string
type T21 = Bar<{ a: (x: string) = > void, b: (x: number) = > void} >.// string & number
Copy the code

When inferring a type that has more than one calling signature (such as a function-overload type), the last signature (presumably the freest all-inclusive signature) is used for inferring. Overloads cannot be resolved based on a list of parameter types.

declare function foo(x: string) :number;
declare function foo(x: number) :string;
declare function foo(x: string | number) :string | number;
type T30 = ReturnType<typeof foo>;  // string | number
Copy the code

The infer statement cannot be used in constraint substatements of normal type parameters:

type ReturnType<T extends(... args:any[]) => infer R> = R;  // Error, not supported
Copy the code

However, we can achieve the same effect by removing the type variable from the constraint and replacing it with a conditional type:

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

Predefined conditional types

TypeScript 2.8 adds predefined conditional types in lib.d.ts:

  • Exclude<T, U>– fromTThe cull can be assigned toUThe type of.
  • Extract<T, U>Extract –TCan be assigned toUThe type of.
  • NonNullable<T>– fromTIn rejectingnullandundefined.
  • ReturnType<T>Gets the function return value type.
  • InstanceType<T>Gets the instance type of the constructor type.

Example

type T00 = Exclude<"a" | "b" | "c" | "d"."a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d"."a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude<string | number | (() = >void), Function>;  // string | number
type T03 = Extract<string | number | (() = >void), Function>;  // (a)= > void

type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() = >string) | string[] | null | undefined>; / / (() = >string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<(a)= > string>;  // string
type T11 = ReturnType<(s: string) = > void>;  // void
type T12 = ReturnType<(<T>() => T) >; / / {}type T13 = ReturnType< (<T extends U, U extends number[] > () => T) >; //number[]
type T14 = ReturnType<typeof f1>; / / {a: number.b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // any
type T17 = ReturnType<string>;  // Error
type T18 = ReturnType<Function>;  // Error

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error
Copy the code

Note: The Exclude type is an implementation of the recommended Diff type. We use the name Exclude to avoid breaking the code that already defines the Diff, and we feel that this name better expresses the semantics of the type. We did not add the

type because it could easily be represented by Pick

>.
,>
,>

8. TypeScript decorator

With the introduction of classes in TypeScript and ES6, there are some scenarios where we need additional features to support annotation or modification of classes and their members. Decorators provide a way for us to add annotations to class declarations and members through metaprogramming syntax. Decorators in Javascript are currently in the second phase of the call for proposals, but are already supported as an experimental feature in TypeScript.

To enable the experimental decorator feature, you must enable the experimentalDecorators compiler option either on the command line or in tsconfig.json:

  • The command line:
tsc --target ES5 --experimentalDecorators
Copy the code
  • tsconfig.json:
{
    "compilerOptions": {
        "target": "ES5"."experimentalDecorators": true}}Copy the code

A decorator

A decorator is a special type of declaration that can be attached to a class declaration, method, accessor, property, or parameter.

Grammar:

  • First define the decorator function:

Defines a decorator function to write extension functions that will be called when the decorator is in use.

  • Use decorators:

The decorator is invoked in the form of @expression, the @ decorator function, before the class or method that needs to be decorated.

Here is an example of using the class decorator (@age) for Cat and Dog classes:

Define @age decorator functions:

function Age(v: number) {
    // The return function is the one that the decorator executes
    return function<T extends {new(... args:any[]) : {}} > (constructor: T): T {
        class PersonAge extends constructor {
            age: number = v;
        }
        returnPersonAge; }}Copy the code

A decorator can be used on both Cat and Dog classes. Neither Cat nor Dog has an age attribute, and when the decorator is called with an argument, it has an age attribute.

@Age(1)
class Cat {
    name = 'Kitty'
}
@Age(2)
class Dog {
    name = 'Little Milk Dog'
}

let c1 = new Cat();
console.log(c1); // Animal {name: "cat ", age: 1}

let d1 = new Dog();
console.log(d1); // Animal {name: "Animal ", age: 2}
Copy the code