Typescript官方文档阅读

208 阅读10分钟

Typescript

本篇文章只是针对Typescript的英文文档的阅读记录,方便后面查阅和学习,对Typescript的掌握程度可能不及看文章的各位大佬的一半;

What is Typescript

TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.

Typescript是一个强类型的语言,建立在JavaScript的基础上,在任何规模上提供更好体验的工具

优点

  • 静态类型检查,可以在编译阶段发现基础错误
  • 更好的语法提示
  • 支持最新的JavaScript新特性

缺点

  • 需要一定的学习成本
  • 对一些非typescript的库支持不是很友好

Everyday Types

Union Types (联合类型)

用来描述可能存在的多种类型

    type numberOrString = number | string;
    function printId(id: numberOrString) {
        console.log(`your id is: ${id}`)
        if (typeof id === 'string') {
            console.log(id.toUpperCase())
        }
    }
    
    printId(1); // ok
    printId('hello'); // ok

Type Aliases

用一个类型别名来直接描述一个对象的形状

    type Point {
        x: number;
        y: number;
    }
    function printCoord(pt:Point) {
        console.log(`x: ${pt.x}, y: ${pt.y}`);
    }
    printCoord({ x: 1, y: 2 });

interface

接口是另一种形式去描述一个对象

    interface Point {
        x: number;
        y: number;
    }
    function printCoord(pt: Point) {
        console.log("The coordinate's x value is " + pt.x);
        console.log("The coordinate's y value is " + pt.y);
     }
    printCoord({ x: 100, y: 100 });

Interface vs Type Alias

  • type不能扩展,需要通过&来合并;
// interface实现
interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}

const bear = getBear() 
bear.name
bear.honey
//  type alias 实现
type Animal = {
  name: string
}

type Bear = Animal & { 
  honey: boolean 
}

const bear = getBear();
bear.name;
bear.honey;
  • 同名接口属性会合并,type alias则会报错
interface Window {
  title: string
}

interface Window {
  ts: TypeScriptAPI
}

const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});

// 下面语句会报错
type Window = {
  title: string
}

type Window = {
  ts: TypeScriptAPI
}
// Error: Duplicate identifier 'Window'.

Type Assertions (类型断言)

当明确知道一个类型时,可以使用断言

// 默认是HTMLElement类型
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

Enums (枚举)

Numeric enums

// 默认 Up值为0,依次递增
enum Direction {
  Up,
  Down,
  Left,
  Right,
}
// 指定从1开始
enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}

String enums

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

Namespaces (命名空间)

随着更多验证器的加入,我们需要一种手段来组织代码,以便于在记录它们类型的同时还不用担心与其它对象产生命名冲突。 因此,我们把验证器包裹到一个命名空间内,而不是把它们放在全局命名空间下

namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }
  const lettersRegexp = /^[A-Za-z]+$/;
  const numberRegexp = /^[0-9]+$/;
  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
      return lettersRegexp.test(s);
    }
  }
  export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
      return s.length === 5 && numberRegexp.test(s);
    }
  }
}
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: Validation.StringValidator } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
for (let s of strings) {
  for (let name in validators) {
    console.log(
      `"${s}" - ${
        validators[name].isAcceptable(s) ? "matches" : "does not match"
      } ${name}`
    );
  }
}

Multi-file namespaces (多文件命名空间)

同名的命名空间的内容会合并到同一个命名空间内, 因为同一个命名空间存在于多个文件,不同文件之间不存在依赖,需要手动加入reference引用标签来告诉编译器文件之间的联系

Validation.ts

namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }
}

LettersOnlyValidator.ts

/// <reference path="Validation.ts" />
namespace Validation {
  const lettersRegexp = /^[A-Za-z]+$/;
  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
      return lettersRegexp.test(s);
    }
  }
}

ZipCodeValidator.ts

/// <reference path="Validation.ts" />
namespace Validation {
  const numberRegexp = /^[0-9]+$/;
  export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
      return s.length === 5 && numberRegexp.test(s);
    }
  }
}

Test.ts

/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: Validation.StringValidator } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
for (let s of strings) {
  for (let name in validators) {
    console.log(
      `"${s}" - ${
        validators[name].isAcceptable(s) ? "matches" : "does not match"
      } ${name}`
    );
  }
}

Ambient Namespaces (外部命名空间)

为了描述不是用TypeScript编写的类库的类型,我们需要声明类库导出的API, 我们通常在 .d.ts里写这些声明

以D3这个库为例: D3.d.ts

declare namespace D3 {
    export interface Selectors {
        select: {
            (selector: string): Selection;
            (element: EventTarget): Selection;
        };
    }

    export interface Event {
        x: number;
        y: number;
    }

    export interface Base extends Selectors {
        event: Event;
    }
}

declare var d3: D3.Base;

Narrowing (类型收窄)

typeof type guards (typeof 守卫)

当函数入参是联合类型时,需要进行类型判断,除非是共有的属性和方法

function padLeft(padding: number | string, input: string) {
    if (typeof padding === "number") {
        return " ".repeat(padding) + input;
    }
    return padding + input;
}

typeof 不能识别出'null', 此时需要真值判断

function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
    for (const s of strs) { // error
     //Object is possibly 'null'.Object is possibly 'null'.
        console.log(s);
    }
    } else if (typeof strs === "string") {
        console.log(strs);
    } else {
    // do nothing
    }
}

Truthiness narrowing (真值收紧)

除了0, NaN, "", 0n, null, undefined, false会转为false,其他都是真值

// 针对上面的问题,可进行如下处理
function printAll(strs: string | string[] | null) {
if ( strs && typeof strs === "object") {
    for (const s of strs) { // error
     //Object is possibly 'null'.Object is possibly 'null'.
        console.log(s);
    }
    } else if (typeof strs === "string") {
        console.log(strs);
    } else {
    // do nothing
    }
}

The 'in' operator narrowing

type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }
 
  return animal.fly();
}

More on Functions

Call Signatures

In JavaScript, functions can have properties in addition to being callable. However, the function type expression syntax doesn’t allow for declaring properties. If we want to describe something callable with properties, we can write a call signature in an object type

当一个函数需要添加特定的属性描述时,则需要使用Call Signature来规定该属性

type DescribableFunc = {
    description: string,
    (arg: string): string;
}

function doSomething(fn: DescribableFunc) {
    console.log(fn.description);
    let s = fn('hello');
    console.log(s);
}

function fn1(name: string) {
    console.log(name);
    return name;
}
fn1.description = 'this is a funciton called fn1';

function fn2(n: string) {
    return n;
}

doSomething(fn1);
doSomething(fn2); // error  Property 'description' is missing

Construct Signatures

JavaScript functions can also be invoked with the new operator. TypeScript refers to these as constructors because they usually create a new object. You can write a construct signature by adding the new keyword in front of a call signature 当需要限制必须有constructor构造器时

type SomeObject = {
    name: string;
}
type SomeConstructor = {
  new (s: string): SomeObject;
};
function createFactory(ctor: SomeConstructor) {
  return new ctor("hello");
}

class MyClass {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}

let m = createFactory(MyClass);
console.log(m.name); // hello

Constraints (泛型约束)

当我们使用泛型时,又必须要满足某些特定条件可以如下操作

// 获取最长的元素
function longest<Type extends { length: number }>(a: Type, b: Type) {
    if (a.length >= b.length) {
        return a;
    }
    return b;
}

longest('1231', '22'); // ok
longest([1,2,3], [3,3,4,5,6]); // ok 
longest(11, 22); // error number is not accessible on length property

Function Overloads (函数重载)

根据参数的个数或者类型去调用不同的函数, 函数实现必须仅跟在重载函数的后面

function len(n: string): number;
function len<T>(n: T[]): number;
function len(n: any): number {
    return n.length;
}

console.log(len('1231313'))
console.log(len([1,2,3]))
console.log(len(['1', '3']))

Rest Parameters and Arguments (剩余参数)

function multiply(n: number, ...m: number[]) {
    return m.map(x => x * n);
}

console.log(multiply(2, 1, 2, 3)) // [2,4,6]

Parameter Destructuring (参数解构)

type ABC = {
    a: number;
    b: number;
    c: number;
}

function fn({a, b, c}: ABC) {
    console.log(a, b, c)
}

fn({ a: 1, b: 2, c: 3 }); // ok
fn({ a: '11', b: 2, c: 3 }); // error

Object Types(对象类型)

基础的声明对象类型

// basic1
function greeting(person: {name: string, age: number}) {
    return 'Hello' + person.name;
}

// use an interface
interface IPerson {
    name: string;
    age: number;
}
function greeting(person: IPerson) {
    return 'Hello' + person.name;
}
// use type alias
type Person {
    name: string;
    age: number;
}
function greet(person: Person) {
    return "Hello " + person.name;
}

Property Modifiers (属性修饰符)

Optional Properties(可选参数)

interface PaintOptions {
    radius: number;
    xPos?: number;
    yPos?: number;
}

function paintShape(opts: PaintOptions) {
    // ....
}
paintShape({ radius: 10 })
paintShape({ radius: 10, xPos: 100 })
paintShape({ radius: 10, yPos: 200 })
paintShape({ radius: 10, xPos: 100, yPos: 200 })

// 通过解构参数给对象参数设置默认值
function paintShape({radius, xPos = 0, yPos = 0} : PaintOptions) {
    console.log('x coordinate at: ', xPos);
    console.log('y coordinate at: ', YPos);
}

// 注意:  参数解构之后  不能指定类型
function draw({raduis: number, xPos: number = 100}) {
    console.log(radius); // cannot find name 'radius' 在js语法中,radius已经被重命名为number,所以radius找不到
    console.log(xPos);
}

readonly Properties (只读属性)

interface ISomeType {
    readonly prop: string;
}
function doSomething(obj: ISomeType) {
    console.log(obj.prop); // we can read from 'obj.prop'
    
    // But we can't re-assign it
    obj.prop = 'hello'
}

// readonly 修饰对象属性时
interface IHome {
    readonly resident: { name: string, age: number }
}
function visit(home: IHome) {
    console.log(home.resident.name)
    home.resident.age++; // this is ok 
    
    home.resident = { // can't write to 'resident'
        age: 10
    }
}

Index Signatures (动态属性)

当不知道有哪些属性时,可以根据数据的格式来描述这些可能的值

// index 类型只能为 string, number, symbol或者这些值的联合类型
// 定义一个字符串数组
interface IStringArray {
    [index: number]: string
}
const myArr: IStringArray = getStringArray();
const secondItem = myArr[1];  // secondItem type为string类型

// 当使用Index Signatures时,是定义个对象所有属性的格式
interface INumberDir {
  [index: string]: number;
  length: number; // ok
  name: string; // name属性则不符合 [index: string]:number的要求
}
// 当需要有多个类型时, 可以使用union types
interface INumberDir {
  [index: string]: number | string;
  length: number; // ok
  name: string; // ok
}

Extending Types (扩展类型)

通过interface的extends 关键字实现扩展

    interface IBasicAddress {
        name?: string;
        street: string;
        city:string;
    }
    // 当需要在basicAddress基础上新增一个属性时
    interface IUnitAddress extends IBadicAddress {
        unit: string;
    }
    
    // 当需要extend多个types时, 以逗号分隔
    interface IUnitAddress extends IType1, IType2 {
        color: string;
    }

Intersection Types (交集类型)

跟interface extends类似, 通过 '&' 操作符实现

interface IColor {
    color: string;
}
interface ICircle {
    radius: number;
}
type ColorCircle = IColor & ICircle;

function draw(circle: ColorCircle) {
    console.log(`color: ${circle.color}, raduis: ${circle.radius}`)
}

draw({ color: 'red', radius: 20 }) // ok 
draw({ color: 'red', raidus: 20 }) // error  raidus拼写错误

Interfaces vs. Intersections

  • 相同点

都可以合并类型

  • 不同点

当合并的类型中存在相同type时,处理效果不一致

  1. interface 直接提示错误(不能同时定义相同的type,格式相同除外)
  2. intersection 会属性定义时不报错,使用时会报错,方法则会合并成联合类型
interface IAnimal {
  name: string;
  sleep: (name: string) => void;
}

interface IName {
  sleep: (name: number) => void;
}

// Interface 'IFish' cannot simultaneously extend types 'IAnimal' and 'IName'.  Named property 'sleep' of types 'IAnimal' and 'IName' are not identical.(
interface IFish extends IAnimal, IName{
  swim: () => void;
}

type IBird = IAnimal & IName; // 不报错

let bird: IBird = {
    name: '111', 
    sleep: (a) => {console.log(a)} // a为 number | string 类型
}

Generic Object Types (泛型类型)

interface Box<T> {
    contents: T;
}

let box: Box<string> = { content: 'hello' };
box.content; // string
let box2: Box = { content: 111 }
box2.content; // number

Type Manipulation (类型处理)

Generics (泛型)

通常写一个函数, 其中输入类型与输出类型相关, 或以某种方式关联时,可以使用泛型

function identity<Type>(arg: Type): Type {
    reutrn args;
}
// 显示指定Type
let  output = identity<string>('hello world')

// 类型推断
let output = identity('hello world');

// generic functions 泛型方法
function identity<Type>(arg: Type): Type {
    return arg;
}
// 指定类型为泛型函数
let myIdentity: <Type>(arg: Type) => Type = identity;

// 也可以通过object literal(对象字面量)形式定义
let myIdentity: { <Type>(arg: Type) : Type } = identity;

// generic interface 泛型接口
interface IGenericIdentityFn {
    <Type>(arg: Type): Type;
}

let myIdentity: IGenericIdentityFn = identity

// 指定类型
interface IGenericType<Type> {
    (arg: Type): Type;
}
let myIdentity: IGenericType<number> = identity

// generic classes
// 需要在tsconfig.json中将strictPropertyInitialization 设置为false
class GenericNumber<Type> {
    zeroValue: Type;
    add: (x: Type, y: Type) => Type;
}
let myNumber = new GenericNumber<number>()
myNumber.zero = 0;
myNumber.add = function(x, y) {
    return x + y;
}

// generic Constraints (泛型约束)
function logging<Type>(arg: Type): Type {
    console.log(arg.length)// error length属性不存在
    return arg;
}

interface ILengthwise {
    length: number;
}

function loggingLen<Type extends ILengthwise>(arg: Type): Type {
    console.log(arg.length)
    return arg;
}
loggingLen('123') // ok
loggingLen([1,2,3]) // ok
loggingLen(123) // error number中没有length属性

// Using Type Parameters in Generic Constraints
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
    return obj[key]
}

let x = { a: 1, b: 2, c: 3 }
getProperty(x, 'a') // ok
getProperty(x, 'm') // error

// using Class Types in Generics 在泛型中使用类类型
function createFactory<Type>(c: { new(): Type } ) {
    return new c();
}

class Animal {
    name: string = 'cat'
}

let a1 = createFactory(Animal);
a1.name; // cat

Keyof Type Operator (keyof 操作符)

type Point = { x: number, y: number }
type P = keyof Point; // same as type P = "x" | "y"

type Arrayish = {
    [index: number]: unknown;
}
type A = keyof Arrayish; // type A = number;

type Mapish = {
    [k: string]: string;
}
type M = keyof Mapish; // type M = string | number
// 在JavaScript中obj[0]和obj['0']是一样的

Typeof Type Operator

type Predicate = (x: unknow) => boolean;
type K = ReturnType<Predicate> // type K = boolean
// 当在方法名上使用ReturnType时

function f() {
    return { x: 3, y: 4 }
}
type P = ReturnType<typeof f>; // type P = {x: number, y: number}

Classes (类)

basic usage

class GoodGreeter {
    name: string;
    constructor(name: string) {
        this.name = name
    }
}

readonly

// Fields with readonly to prevent modify outside the constructor
class Greeter {
    readonly name: string  = 'hello';
    constructor(name?: string) {
        if (name !== undefined) {
            this.name = name;
        }
    }
}
let g = new Greeter()
g.name = 'world' // error

let g2 = new Greeter('hello world'); // ok
g2.name; // hello world

methods

class Greeter {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    greet(): void {
        console.log(`hello: ${this.name}`)
    }
}

Getters/Setters

  • 如果只存在get,没有定义set, 则该属性默认为readonly
  • 如果setter的入参没有定义类型, 默认为getter 的返回值类型
  • getter和setter必须有相同的成员可见度
    class Gretter {
        name: string = 'hello';
        get name() {
            return this.name
        }
        set name(newName) {
            this.name = newName;
        }
    }

Class Heritage (类实现)

    interface Pingable {
        ping(): void;
    }
    
    class Sonar implements Pingable {
        ping() { // 必须要实现interface定义的方法
            console.log('ping....')
        }
    }

extends clause (类继承)

class Base {
    greet() {
        console.log('hello world');
    }
    say() {
        console.log('call say().... ');
    }
}
class Derived extends Base {
    greet(name?: string) { // 重写greet方法 ,必须包含greet
          if(name === undefined) {
              super.greet() // 调用父类的方法
          } else {
              console.log(`Hello, ${name}`)
          }
    }
}

const d = new Derived();
d.greet(); // hello world
d.greet('Jack'); // hello Jack
d.say(); // call say()....

Member visibility (成员可见性)

  • public (The default visibility)

可以在类,子类,实例中访问

class Greeter {
    public greet() {
        console.log('Hi!')
    }
}
const g = new Greeter()
g.greet();
  • protected

仅能在类和子类中访问

class Greeter {
    public greet() {
        console.log(`Hello, ${this.getName()}`);
    }
    protected getName() {
        return 'mike';
    }
}

class SpecialGreeter extends Greeter {
    public howdy() {
        console.log(`Howdy, ${this.getName()}`)
    }
}
const g = new SpecialGreeter();
g.greet(); // ok
g.howdy(); // ok
g.getName(); // error

  • private

只能在当前类中使用,和protected类似,但不能在子类中使用

class Base {
    private x: number = 1;
}
const b = new Base();
console.log(b.x); // error Property 'x' only accessible within class 'Base'

class Derived extends Base {
    showX() {
        console.log(this.x); // error
    }
}

Static Members (静态成员)

They can be accessed through the class constructor object itself

class MyClass {
    static x = 1;
    static printX() {
        console.log(MyClass.x)
    }
}
console.log(MyClass.x);
MyClass.printX();
  • Static menbers ara also inherited
class Base {
    static getGreeting(){
        return 'hello world';
    }
}
class Derived extends Base {
    myGreeting = Derived.getGreeting();
}
const d = new Derived();
console.log(d.myGreeting); // hello world
  • Special Static Names
    class S {
        static name = 'S'; // error 与 Function.name冲突, 类似的非法属性name, length, call
    }

Abstract Members (抽象类)

抽象方法和属性只能定义在抽象类中, 抽象类不能通过new实例化

abstract class Base {
    abstract getName(): string;
    printName() {
        console.log(`Hello, ${this.getName()}`);
    }
}

const b = new Base(); // error  cann't create instance of an abstract class.

class Derived extends Base {
    getName() {
        return 'world';
    }
}

const d = new Derived();
d.printName(); // Hello world

Reference - 工具泛型

Partial<Type>

指定为特定类型的子集

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

function updateTodo(todo: Todo, fieldToUpdate: Partial<Todo>) {
    return {...todo, ...fieldToUpdate }
}

let todo1 = {
    title: 'test',
    description: 'this is a desc'
}

let todo2 = updateTodo(todo1, { title: 'test2' });
console.log(todo2); // { title: 'test2', description: 'this is a desc'}
let todo3 = updateTodo(todo1, { name: 'cc' }); // error

Required<Type>

设置特定类型的所有属性为必填

interface Props {
    a?: number;
    b?: number;
}

let p1: Props = { a: 1 }; // ok
let p2: Required<Props> = { a: 2 }; // error  p2必须满足a,b都存在

Readonly

设置特定类型的所有属性为只读

interface Props {
    title: string
}

let p1: Props = { title: '11111' };
p1.title = '2222'; // ok 
let p2: Readonly<Props> = { title: 'test' };
p2.title = '33333'; // error

Record<Keys, Type>

适用于对map类型的数据限制

interface CatInfo {
  age: number;
  breed: string;
}
 
type CatName = "miffy" | "boris" | "mordred";
 
const cats: Record<CatName, CatInfo> = {
  miffy: { age: 10, breed: "Persian" },
  boris: { age: 5, breed: "Maine Coon" },
  mordred: { age: 16, breed: "British Shorthair" },
};

Pick<Type, Keys>

从指定的类型中摘取出指定的属性

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}
 
type TodoPreview = Pick<Todo, "title" | "completed">;
 
const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};

Omit<Type, Keys>

从指定类型中剔除指定的属性

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}
 
type TodoPreview = Omit<Todo, "title">;
 
const todo: TodoPreview = {
  description: 'desc...',
  completed: false,
};

Exclude<UnionType, ExcludedMembers>

从unionType中排除指定成员

type T0 = Exclude<"a" | "b" | "c", "a">; // type T0 = "b" | "c"
     
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // type T1 = "c"     

type T2 = Exclude<string | number | (() => void), Function>; // type T2 = string | number

NonNullable

从类型中排除 null和undefined

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

Decorator (装饰器)

装饰器是一种特殊类型的声明,它能够被附加到类声明方法, 访问符属性参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入

注意 装饰器是一个实验性特性,需要增加相关配置适配

  • 命令行
tsc --target ES5 --experimentalDecorators
  • tsconfig.json
{ 
    "compilerOptions": { 
        "target": "ES5", 
        "experimentalDecorators": true 
     } 
}

Decorator Factory (装饰器工厂)

为了允许装饰者接受参数,我们需要使用所谓的装饰工厂。装饰工厂只是一个包装器功能,可以返回装饰函数本身

function decoratorFactory(name: string) {
    return function(constructor: Function) {
      console.log(`decorator function called with : ${name}`);
    }
}

@decoratorFactory('testName')
class ClassWithDecoratorFactory {

}
// "decorator function called with : testName"

Class Decorator (类装饰器)

The class decorator is applied to the constructor of the class and can be used to observe, modify, or replace a class definition

类装饰器应用于类的构造函数,可用于观察,修改或替换类定义

  • class Decorator执行顺序
// decorator ==> class constrcutor
function decoratorFn(ctor: Function) {
    console.log('decorator evaluated.....');
}
@decoratorFn
class Base {
    constructor() {
        console.log('construtor run....');
    }
}

let  b = new Base();
// decorator evaluated.....
// construtor run....
  • 类构造器的重载
function classDecorator<T extends { new(...arg: any[]) : {}}>(constructor: T) {
    console.log('classDecorator evaluated.....')
    return class extends constructor {
        test = "test decorator";
    }
}
@classDecorator
class BugReport {
    title: string;
    type = 'report';
    constructor(t: string) {
        this.title = t;
    }
}
let r = new BugReport('dark mode');
console.log(r); // { type: 'report', title: 'dark mode', test: 'test decorator'}
console.log(r.test); // error  Property 'test' does not exist on type 'BugReport'
// 因为decorator不能修改Typescript的Type

// 方案一:可以通过mixins来解决
function classDecorator<T extends { new(...arg: any[]) : {}}>(constructor: T) {
    console.log('classDecorator evaluated.....')
    return class extends constructor {
        test = "test decorator";
    }
}
let NewReport = classDecorator(BugReport);
type NewReport = InstanceType<typeof NewReport>;
let nr = new NewReport('light mode'); // { type: 'report', title: 'light mode', test: 'test decorator'}
console.log(nr); 
console.log(nr.test); // no errors

// 方案二: 通过interface tricks
function classDecorator<T extends { new(...arg: any[]) : {}}>(constructor: T) {
    console.log('classDecorator evaluated.....')
    return class extends constructor {
        test = "test decorator";
    }
}
interface classInterface {
    test: string;
    title: string;
    type: string;
}

//trick
interface BugReport extends classInterface { };

@classDecorator
class BugReport {
    title: string;
    type = 'report';
    constructor(t: string) {
        this.title = t;
    }
}
let r = new BugReport('dark mode');
console.log(r.test); // no errors 

Methods Decorator (方法装饰器)

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 成员的名字
  • 成员的属性描述符 descriptor
function log() {
    return function(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<(...arg: any[]) => any>) {
        let method = descriptor.value;
        descriptor.value = function(...arg: any[]) {
            const params: string = arg.join('');
            const result = method!.call(this, arg);
            console.log(`Call ${propertyKey} (${params}) => ${result}`)
            return result;
        }
    }
}

class Greeter {
    greeting: string;
    constructor(s: string) {
        this.greeting = s;
    }
    @log()
    greet(name: string) {
        return `Hello , ${name}`
    }
}

let g = new Greeter('Jack');
g.greet('Jack');
g.greet('limei');
// "Call greet (Jack) => Hello , Jack"
// "Call greet (limei) => Hello , limei"

Accessor Decorator (存取器装饰器)

属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 成员的名字
  • 成员的属性描述符 descriptor
class Point {
  private _x: number;
  private _y: number;
  constructor(x: number, y: number) {
    this._x = x;
    this._y = y;
  }
 
  @enumerable(false)
  get x() {
    return this._x;
  }
 
  @enumerable(true)
  get y() {
    return this._y;
  }
}

function enumerable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = value;
  };
}
// 默认存取器是不可枚举的,可以通过装饰器修改存取器的属性描述符
let p = new Point(1,2);
for (var key in p){
  console.log(key);
}
// _x
// _y
// y

Property Decorator (属性装饰器)

属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 成员的名字
function format() {
    return function(target: any, propertyKey: string) {
        let message: string;
        const descriptor = {
            get() {
                return `Hello , ${message}`
            },
            set(value: string) {
                message = value
            },
            configurable: true,
            enumerable: true
        }
        Object.defineProperty(target, propertyKey, descriptor);
    }
}

class Greeter {
    @format()
    greeting: string;
    constructor(s: string) {
        this.greeting = s;
    }
    greet() {
        console.log(this.greeting);
    }
}

let g = new Greeter('Jack');
console.log(g.greeting); // Hello , Jack
g.greet(); // Hello , Jack

Parameter Decorators (参数装饰器)

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 成员的名字
  • 参数在参数列表中的下标
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");

interface IFunction {
  (...arg: any[]): any
}
 
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
  let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata( requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate() {
  return function (target: any, propertyName: string, descriptor: TypedPropertyDescriptor<IFunction>) {
    let method = descriptor.value!;
  
    descriptor.value = function (...arg: any[]) {
      let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
      if (requiredParameters) {
        for (let parameterIndex of requiredParameters) {
          if (parameterIndex >= arg.length || arg[parameterIndex] === undefined) {
            throw new Error("Missing required argument.");
          }
        }
      }
      return method.apply(this, arg);
    };
  }
}


class BugReport {
  type = "report";
  title: string;
 
  constructor(t: string) {
    this.title = t;
  }
 
  @validate()
  print(@required verbose: boolean) {
    if (verbose) {
      return `type: ${this.type}\ntitle: ${this.title}`;
    } else {
     return this.title; 
    }
  }
}
let b = new BugReport('hello world');
console.log(b.print(false)); // hello world
b.print(); // error
// Missing required argument.

执行顺序

  • 整体执行顺序 propertyDecorator => accessorDecorator => paramDecorator => methodDecorator => classDecorator
  • 当存在方法和方法参数装饰器时,参数装饰器先执行
  • 当存在方法和属性装饰器时, 属性装饰器先执行
  • 当存在多个参数装饰器时,从后往前执行
  • 类装饰器始终是最后一个执行
  • 同类型装饰器按照从下往上,从右往左执行
function classDecorator1(ctor: Function) {
  console.log('classDecorator1 evaluated.....')
}

function methodDecorator1(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log('methodDecorator1 evaluated.....')
}

function paramDecorator1(target: Object, propertyKey: string, parameterIndex: number) {
  console.log('paramDecorator1  evaluated.....')
}

function propertyDecorator1(target: Object, propertyKey: string) {
  console.log('propertyDecorator1  evaluated.....')
}

function accessorDecorator1(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log('accessorDecorator1  evaluated.....')
}

@classDecorator1
@classDecorator2
class Greeter {
  @propertyDecorator1
  @propertyDecorator2
  _title: string;
  @accessorDecorator1
  @accessorDecorator2
  get title() {
    return this._title
  }
  constructor(){
    this._title = 'hello'
  }
  @methodDecorator1
  @methodDecorator2
  greet(@paramDecorator1 msg: string, @paramDecorator2 id: number) {
    console.log(`greet ${this.title}`)
  }
}
// propertyDecorator2 evaluated.....
// propertyDecorator1 evaluated.....
// accessorDecorator2 evaluated.....
// accessorDecorator1 evaluated.....
// paramDecorator2 evaluated.....
// paramDecorator1 evaluated.....
// methodDecorator2 evaluated.....
// methodDecorator1 evaluated.....
// classDecorator2 evaluated.....
// classDecorator1 evaluated.....

Declaration Merging (声明合并)

针对同一个名称的两个独立声明合并为单一声明

声明类型NamespaceTypevalue
Namespace
Class
Enum
Interface
Type Alias
Function
Variable

Merging Interfaces (接口合并)

同名Interface中的非函数成员必须是唯一的,如果同时存在的话,则类型必须一致, 否则报错

  • 属性合并
interface IA {
  name: string;
}

interface IA {
  title: string;
}

interface IA {
    name: number; // error 同名属性类型必须一致
}

// 以上等价于
interface IA {
    name: string;
    title: string;
}
  • 方法合并

注意每组接口里的声明顺序保持不变,但各组接口之间的顺序是后来的接口重载出现在靠前位置。

interface IA {
  greet(name: string): void;
}

interface IA {
  greet(name: number): void;
}
// 以上等价于
interface IA {
    greet(name: number): void;
    greet(name: string): void;
}

let fn: IA = {
  greet(name: string | number) {},
}

Merging Namespaces (命名空间合并)

  • 对于命名空间的合并,模块导出的同名接口进行合并,构成单一命名空间内含合并后的接口
  • 对于命名空间里值的合并,如果当前已经存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里
  • 非导出成员仅在其原有的(合并前的)命名空间内可见
namespace Animals {
    export class Zebra { }
}

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

等同于

namespace Animals {
  export interface Legged {
    numberOfLegs: number;
  }
  export class Zebra {}
  export class Dog {}
}

Merging Namespaces with class

适用于定义内部类

class Base {
  label:Base.Label;
  constructor(label: Base.Label) {
    this.label = label;
  }
}

namespace Base {
  export class Label {}
}

Merging Namespaces with function

function buildLabel(name: string): string {
  return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
  export let suffix = "!";
  export let prefix = "Hello, ";
}
console.log(buildLabel("Sam Smith")); // Hello, Sam Smith!

Merging Namespaces with Enum

enum Color {
  red = 1,
  green = 2,
  blue = 4,
}
namespace Color {
  export function mixColor(colorName: string) {
    if (colorName == "yellow") {
      return Color.red + Color.green;
    } else if (colorName == "white") {
      return Color.red + Color.green + Color.blue;
    } else if (colorName == "magenta") {
      return Color.red + Color.blue;
    } else if (colorName == "cyan") {
      return Color.green + Color.blue;
    } else {
      return Color.red;
    }
  }
}

Mixins (混入)

混入是在对象之间共享行为的方式


class BaseMixin {
  name = 'BaseMixin';
  title: string;
  constructor(t: string) {
    this.title = t;
  }
}

type Constructor = new (...arg: any[]) => {}

function Greet<T extends Constructor>(Base: T) {
  return class extends Base {
    constructor(...arg: any[]) {
      super(arg);
    }
  }
}

const greetCls = Greet(BaseEntity)
const g = new greetCls('greet');
console.log(g.title); // greet
console.log(g.name); // BaseMixin

Constrained Mixins

带约束的混入

type GConstructor<T = {}> = new (...args: any[]) => T;
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;

function Jumpable<TBase extends Positionable>(Base: TBase) {
  return class Jumpable extends Base {
    jump() {
      // This mixin will only work if it is passed a base
      // class which has setPos defined because of the
      // Positionable constraint.
      this.setPos(0, 20);
    }
  };
}
class BaseJump {
  setPos(x: number, y: number) {
    console.log(`x: ${x}, y: ${y}`);
  }
}
let JumpCls = Jumpable(BaseJump);
let j = new JumpCls()
j.jump(); // x: 0, y: 20

Alternative Pattern

class Jumpable {
  jump() {
    console.log('jump');
  }
}

class Duckable {
  duck() {
    console.log('duck');
  }
}

class Sprite {
  x = 0;
  y = 2;
}
// 定义同名接口让typescript可以识别出混入类中的属性
interface Sprite extends Jumpable, Duckable {}

applyMixins(Sprite, [Jumpable, Duckable])
let s = new Sprite();
s.duck();
s.jump();

function applyMixins(derivedCtor: any, constructors: any[]) {
  constructors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      Object.defineProperty(
        derivedCtor.prototype,
        name,
        Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
          Object.create(null)
      );
    });
  });
}

Type Compatibility (类型兼容)

TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性

interface Pet {
  name: string;
}
let pet: Pet;
// dog's inferred type is { name: string; owner: string; }
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
pet = dog; // ok

Compare with Functions

  • 参数不一致,返回值一样
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
// 要查看`x`是否能赋值给`y`,首先看它们的参数列表。 `x`的每个参数必须能在`y`里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型
y = x; // OK
x = y; // Error
  • 参数一致, 返回值不一样
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});
// 类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型
x = y; // OK
y = x; // Error, because x() lacks a location property

Enum

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的

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

let s = Status.Ready;
s = 0; // ok 
s = Color.Red;  // Error

Best Practices (最佳实践)

Strict configuration (严格模式)

tsconfig.json

{
        "forceConsitentCasingInFileNames": true, // 保证文件名一致
        "noImplicitReturns": true, // 当存在不明确的return语句时报错
        "strict": true, // 开启严格模式
        "noUnusedLocals": true, // 无用的变量
    }

其中strict:true包含了四个部分

  • noImplicitThis: this隐式转换为any时,提示报错
class Rectangle {
  width: number;
  height: number;
 
  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }
 
  getAreaFunction() {
    return function () {
      // 'this' implicitly has type 'any' because it does not have a type annotation.
      return this.width * this.height; // error 
    };
  }
}
  • noImplicitAny: 当存在隐式转换为any时报错
function fn(s) { // error Parameter 's' implicitly has an 'any' type.
  console.log(s.subtr(3));
}
  • strictNullChecks: 针对null和undefined操作限制
// 当strictNullChecks为true时
declare const loggedInUsername: string;
 
const users = [
  { name: "Oby", age: 12 },
  { name: "Heera", age: 32 },
];
 
const loggedInUser = users.find((u) => u.name === loggedInUsername);
console.log(loggedInUser.age); // error loggedInUser可能为undefined
// 通过'!'告诉编译器肯定存在
console.log(loggedInUser!.age);
// 或者做限制
console.log(loggedInUser && loggedInUser.age);
  • alwaysStrict: 始终开启ECMAScript的严格模式, 并且为每个文件增加"use strict"

General types - prefer to use primitive types

对于基础类型使用number, string, boolean,而不是使用String,Number, Boolean装饰类

Type inference

善用typescript的类型推断, 不需要每次都明确指定类型

Callback params

回调函数尽量不要设置可选参数

Function parameters

当函数参数存在多个相同类型的入参时, 可以使用一个对象形式

// cal方法调用时需要注意参数的顺序
function cal(x: string, y: string, z: string) {}
// cal2 则不需要关心参数的顺序
function cal2(data: {x: string, y: string; z: string}) {}

Overloads - Ordering

函数重载的顺序应该从大范围到具体范围

// any => person => worker
interface Person {}
interface Worker extends Person {}
function tun (a: any) : any;
function tun (p: Person) : string;
function tun (w: Worker) : number;
function tun (a: any): any {
    console.log(a)
}

巧用typeof优于自定义类型

当我们使用的数据是数组的某一项时,可以如下操作

const list = [
    { title: 'xxxx1', msg: 'yyyy1' },
    { title: 'xxxx2', msg: 'yyyy2' },
    { title: 'xxxx3', msg: 'yyyy3' },
]

// 通过typeof 来设置类型
type ListItem = typeof list[0];

function showItem(item: ListItem) {
    console.log(item.title, item.msg);
}

巧用类型断言

当我们初始化时是一个空对象时, ts会给提示缺少对应属性

interface IPerson {
    name: string;
    age: string;
}
// Type '{}' is missing the following properties from type 'IPerson': name, age
let person: IPerson = {}; // error

此时我们需要告诉编译器 person 就是一个IPerson的类型

interface IPerson {
    name: string;
    age: string;
}
let person: IPerson = {} as IPerson; // no error

type-challenges

4 - Pick

interface ITodo {
    name: string;
    title: string;
    desc: string;
}

type MyPick<T, K extends keyof T> = {
    [key in K]: T[key]
}

type todoPreview2 = MyPick<ITodo, 'name' | 'desc'>;

let todo2: todoPreview2 = {
    name: 'xxxx',
    desc: 'yyyy'
}

7 - Readonly

interface ITodo {
    name: string;
    title: string;
}
type MyReadonly<T> = {
    readonly [Key in keyof T]: T[Key]
}

const todo :MyReadonly<ITodo> = {
    name: 'xxxx',
    title: 'xxxx'
}
todo.name = '1111'; // error
todo.title = '2222'; // error

11 - TupleToObject

const tuple = ['sss', 'yyyy'] as const
type INumber = string | number
type TupleToObject<T extends readonly INumber[]> = {
  [K in T[number]]: K
}

type s = TupleToObject<typeof tuple>;

let obj: s = {
    sss: 'sss',
    yyyy: 'yyyy'
}

14 - First of Array

infer 关键字表示待推断的类型变量

type First<T extends any[]> = T extends [infer P, ...any[]] ? P : never;

type num = First<[1,2,3]>; // 1
type fn = First<[() => 123, { a: string }]>; // () => 123
type neverType = First<[]>; // never
type undefinedType = First<[undefined]>; // undefined

infer使用,获取参数的类型,如果参数是普通类型,则返回该类型,如果是函数的话,则返回类型是函数入参的类型

interface User {
    name: string;
    title: string;
}

type Func = (user: User) => void;

type ParamType<T> = T extends (arg: infer P) => any ? P : T;

type s = ParamType<string>;
let ss:s  = '1111';

type Fn = ParamType<Func>;
let user:Fn = {
    name: 'xxx',
    title: 'ttt'
}

18 - Length of Tuple

Tuple实际就是一个只读的数组,可以通过这个来限制入参

type Length<T extends readonly any[]> = T['length'];
const tesla = ['tesla', 'model 3', 'model X', 'model Y'] as const
const spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT'] as const
type num = Length<typeof tesla>; // 4
type num2 = Length<typeof spaceX>; // 5

43 - Exclude

此处利用Typescript中Distributive Conditional Types(分发条件类型)

type MyExclude<T, U> = T extends U ? never : T;

type t1 = MyExclude<'a' | 'b' | 'c', 'a'>; // "b" | "c"
type t2 = MyExclude<string | number | (() => void), Function>; // string | number
type t3 = MyExclude<'a' | 'b' | 'c', 'ad'>; // "a" | "b" | "c"

189 - Awaited

Type中的递归调用

type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer P> ? MyAwaited<P> : T;

type X = Promise<string>
type Y = Promise<{ field: number }>
type Z = Promise<Promise<string | number>>
type Z1 = Promise<Promise<Promise<string | boolean>>>
type T = { then: (onfulfilled: (arg: number) => any) => any }

type XX = MyAwaited<X>; // string
type YY = MyAwaited<Y>; // { filed: number } 
type ZZ = MyAwaited<Z>; // string | number
type ZZ1 = MyAwaited<Z1>; // string | boolean
type TT = MyAwaited<T>; // number

268 - if

boolean 条件判断 通过extends 判断

type If<C extends boolean, T, F> = C extends true ? T : F;
type t1 = If<true, 'a', 'b'>; // 'a'
type t2 = If<false, 'a', 2>; // 2

533 - Concat

type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U]
type cc = Concat<[], []>; // []
type dd = Concat<[], [1]>; // [1]
type ee = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]
type ff = Concat<['1', 2, '3'], [false, boolean, '4']>; // ['1', 2, '3', false, boolean, '4']

898 - Includes

import type { Equal } from '@type-challenges/utils'
type Includes<T extends readonly any[], U> = T extends [infer P, ...infer Rest] ? Equal<P,U> extends true ? true : Includes<Rest, U> : false;

type a1 = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Kars'>; // true
type a2 = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'>;  // false
type a3 = Includes<[boolean, 2, 3, 5, 6, 7], false>; // false

3312 - parameters

type MyParameters<T extends (...args: any[]) => any> = T extends (...arg: infer P) => any ? P : never;

const foo = (arg1: string, arg2: number): void => {}
const bar = (arg1: boolean, arg2: { a: 'A' }): void => {}
const baz = (): void => {}

type t1 = MyParameters<typeof foo>; // [string, number]
type t2 = MyParameters<typeof bar>; // [boolean, { a: 'A' }]
type t3 = MyParameters<typeof baz>; // []

扩展阅读

基础语法

编码风格