This is the second article, TypeScript based on a series of introductory TypeScript | TypeScript base (a), :

  • The base type
  • interface
  • class
  • function
  • The generic
  • The enumeration

Type inference

This section introduces type inference in TypeScript. That is, where and how types are inferred.

Best generic type

When you need to infer a type from several expressions, the types of those expressions are used to infer the most appropriate generic type.

let x = [0.1.null];
Copy the code

To infer the type of X, we must consider the types of all elements. There are two options: number and NULL. The compute common type algorithm considers all candidate types and gives a type that is compatible with all candidate types.

let zoo = [new Rhino(), new Elephant(), new Snake()];
Copy the code

Here, we want zoo to be inferred as Animal[], but there are no objects in this array that are of Animal type, so we cannot infer this result. To correct this, we need to specify the type explicitly when candidate types are not available:

let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
Copy the code

If there is no find the best general type, type inference as an associative array type, the result of the (Rhino | Elephant | Snake) [].

Compare two functions

Relatively easy to understand when comparing primitive types with object types, the problem is how to determine whether the two functions are compatible.

let x = (a: number) = > 0;
let y = (b: number, s: string) = > 0;

y = x; // OK
x = y; // Error
Copy the code

Each parameter of x must have a corresponding type of parameter in y. Note that it doesn’t matter if the parameters have the same name, just their type. Here, each argument to x has a corresponding argument in y, so assignment is allowed.

The second assignment error, because y has a required second argument, but x does not, so assignment is not allowed.

Let’s look at how to handle return value types by creating two functions that differ only in their return value types.

let x = (a)= > ({name: 'Alice'});
let y = (a)= > ({name: 'Alice', location: 'Seattle'});

x = y; // OK
y = x; // Error, because x() lacks a location property
Copy the code

Function parameters are bidirectional covariant

Don’t know much about

When comparing function parameter types, the assignment succeeds only if the source function parameter can be assigned to the target function or vice versa. This is unstable because the caller may pass in a function with more precise type information, but call the function with less precise type information.

enum EventType { Mouse, Keyboard }

interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
    /* ... */
}

// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));

// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));

// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));
Copy the code

Function overloading

For overloaded functions, each overload of the source function finds the corresponding function signature on the target function.

The enumeration

Enumeration types are compatible with numeric types, and numeric types are compatible with enumeration types. Different enumeration types are incompatible.

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

let status = Status.Ready;
status = Color.Green;  // Error
Copy the code

class

Classes are similar to object literals and interfaces, but with one difference: classes have types for static parts and instance parts. When objects of two class types are compared, only instance members are compared. Static members and constructors are outside the scope of the comparison.

class Animal {
    feet: number;
    constructor(name: string, numFeet: number) {}}class Size {
    feet: number;
    constructor(numFeet: number) {}}let a: Animal;
let s: Size;

a = s;  // OK
s = a;  // OK
Copy the code

Private and protected members of the class

The generic

interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y;  // OK, because y matches structure of x
Copy the code

A generic type is used as if it were not a generic type.

interface NotEmpty<T> {
    data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y;  // Error, because x and y are not compatible
Copy the code

High-level types

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

We mostly see the use of cross types in mixins or other places that don’t fit into a typical object-oriented model.

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. It represents either type A or type B

A conjunction type means 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

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
let pet = getSmallPet();

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

User-defined type protection

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 protection makes this a reality. Type protection is expressions that are checked at run time to make sure the type is in a scope. To define a type protection, 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.

typeofType of protection

Now we don’t have to abstract typeof x === “number” as a function, because TypeScript recognizes it as type-protection. 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 protections 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-protected.

instanceofType of protection

Instanceof type protection is a way to refine types through constructors.

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

A type that can be null

TypeScript has two special types, null and undefined, which have values null and undefined, respectively.

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.

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 | is 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, using strictNullChecks

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 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.

Interfaces vs. type aliases

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

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.

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.

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 protection, 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

Numeric literal type

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

Discriminated Unions

You can combine singletons, union types, type protection, and type aliases to create a high-level pattern called recognizable union, also known as label union or algebraic data types.

TypeScript is based on existing JavaScript patterns. It has three elements:

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. Now let’s combine them together

type Shape = Square | Rectangle | Circle;
Copy the code
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

Index types

Using index types, the compiler can examine code that uses dynamic attribute names.

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.

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

Keyof Person is can completely with the ‘name’ | ‘age’ replace each other. The difference is if you add other attributes to Person.

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.

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

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

The module

A note about terminology: It’s important to note that terms have changed in TypeScript 1.5. “Internal modules” are now called “namespaces”. “External modules” are now shortened to “modules” in keeping with ECMAScript 2015 terminology (that is, module X {is equivalent to namespace X {as recommended).

export

Export declaration

Any declaration (such as a variable, function, class, type alias, or interface) can be exported by adding the export keyword.

export interface StringValidator {
    isAcceptable(s: string) :boolean;
}
export const numberRegexp = / ^ [0-9] + $/;
Copy the code

Export statement

class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5&& numberRegexp.test(s); }}export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
Copy the code

To export

We often extend other modules and export only parts of that module. The re-export feature does not import that module in the current module or define a new local variable.

export class ParseIntBasedZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && parseInt(s).toString() === s; }}// Export the original validator but rename it
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";
Copy the code

Or a module can wrap multiple modules and syndicate their exported content with the syntax: export * from “module”.

export * from "./StringValidator"; // exports interface StringValidator
export * from "./LettersOnlyValidator"; // exports class LettersOnlyValidator
export * from "./ZipCodeValidator";  // exports class ZipCodeValidator
Copy the code

The import

Import one of the exported contents in a module

import { ZipCodeValidator } from "./ZipCodeValidator";
let myValidator = new ZipCodeValidator();
Copy the code

You can rename the imported content

import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();
Copy the code

Import the entire module into a variable and access the exported part of the module through it

import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();
Copy the code

Import modules with side effects

Although this is not recommended, some modules will set some global state for other modules to use. These modules may not have any exports or the user may not care about its exports at all. Use the following method to import such modules:

import "./my-module.js";
Copy the code

The default is derived

Each module can have a default export. The default export uses the default keyword tag; And a module can only have one default export. A special import form is required to import the default export.

// JQuery.d.ts
declare let $: JQuery;
export default $;

// App.ts
import $ from "JQuery";
$("button.continue").html( "Next Step..." );
Copy the code

export =import = require()

Both CommonJS and AMD exports can be assigned to an object, in which case it acts like the export default in the ES6 syntax. The export Default syntax is not compatible with CommonJS and AMD exports, although it has similar functions.

To support CommonJS and AMD exports, TypeScript provides export = syntax.

Export = Syntax defines the export object of a module. The term object here refers to a class, interface, namespace, function, or enumeration.

To export a module using export =, you must import the module using TypeScript’s special syntax import Module = require(“module”).

// ZipCodeValidator.ts
let numberRegexp = / ^ [0-9] + $/;
class ZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5&& numberRegexp.test(s); }}export = ZipCodeValidator;

// Test.ts
import zip = require("./ZipCodeValidator");
Copy the code

Use other JavaScript libraries

To describe the types of non-typescript libraries, we need to declare the apis exposed by the libraries.

We call it a declaration because it is not an implementation of an “external program.” They are usually defined in.d.ts files. If you are familiar with C/C++, you can think of them as.h files. Let’s look at some examples.

// node.d.ts
declare module "url" {
    export interfaceUrl { protocol? :string; hostname? :string; pathname? :string;
    }

    export function parse(urlStr: string, parseQueryString? , slashesDenoteHost?) :Url;
}

declare module "path" {
    export function normalize(p: string) :string;
    export function join(. paths:any[]) :string;
    export let sep: string;
}
Copy the code

Direct use of

/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");
Copy the code

External module abbreviation

If you don’t want to spend time writing a declaration before using a new module, you can use the shortened form of the declaration so that it can be used quickly.

// declarations.d.ts
declare module "hot-new-module";
Copy the code

All exports in the shorthand module will be of type any.

import x, {y} from "hot-new-module";
x(y);
Copy the code

The module declares wildcards

Some module loaders such as SystemJS and AMD support importing non-javascript content. They usually use a prefix or suffix to indicate special loading syntax. The module declares that wildcards can be used to represent these cases.

declare module"*! text" {const content: string;
    export default content;
}
// Some do it the other way around.
declare module"json! * "{const value: any;
    export default value;
}
Copy the code

The import

import fileContent from "./xyz.txt! text";
import data from "json! http://example.com/data.json";
console.log(data, fileContent);
Copy the code

A statement to merge

Declaration merge is when the compiler merges two separate declarations for the same name into a single declaration. The combined declaration has both the features of the original two declarations. Any number of declarations can be combined; Not limited to two declarations.

Combined interface

The simplest and most common type of declared merge is interface merge. Basically, the mechanism of merging is to put the members of both parties into an interface with the same name.

interface Box {
    height: number;
    width: number;
}

interface Box {
    scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10};
Copy the code

And as

interface Cloner {
    clone(animal: Animal): Animal;
}

interface Cloner {
    clone(animal: Sheep): Sheep;
}

interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
}
Copy the code

After the merger

interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
    clone(animal: Sheep): Sheep;
    clone(animal: Animal): Animal;
}
Copy the code

One exception to this rule is when a special function signature occurs. If one of the parameters in the signature is of a single string literal type (i.e., a joint type that is not a string literal), it will be promoted to the top of the overload list.

interface Document {
    createElement(tagName: any): Element;
}
interface Document {
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
    createElement(tagName: string): HTMLElement;
    createElement(tagName: "canvas"): HTMLCanvasElement;
}
Copy the code

After the merge promotion

interface Document {
    createElement(tagName: "canvas"): HTMLCanvasElement;
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
    createElement(tagName: string): HTMLElement;
    createElement(tagName: any): Element;
}
Copy the code

Merge namespace

Like interfaces, namespaces with the same name merge their members. Namespaces create namespaces and values, and we need to know how they are combined.

For the merging of namespaces, the interfaces exported by modules are merged to form a single namespace containing the merged interfaces.

For merging values in a namespace, if a namespace with the given name already exists, the exported members of the subsequent namespace are added to the module that already exists.

namespace Animals {
    export class Zebra { }
}

namespace Animals {
    export interface Legged { numberOfLegs: number; }
    export class Dog { }
}
Copy the code

Is equivalent to

namespace Animals {
    export interface Legged { numberOfLegs: number; }

    export class Zebra { }
    export class Dog { }
}
Copy the code

In addition to these merges, you also need to understand how non-exported members are handled. Non-exported members are visible only in their original (pre-merge) namespace. This means that after the merge, members merged from other namespaces cannot access non-exported members.

namespace Animal {
    let haveMuscles = true;

    export function animalsHaveMuscles() {
        returnhaveMuscles; }}namespace Animal {
    export function doAnimalsHaveMuscles() {
        return haveMuscles;  // Error, because haveMuscles is not accessible here}}Copy the code

Welcome to the public account Yunying Sky