TypeScript 学习笔记

304 阅读20分钟

学习三宝

--------需熟读--------

协变与逆变

子类型

比如考虑如下接口:

interface Animal {
  age: number
}

interface Dog extends Animal {
  bark(): void
}

在这个例子中,Animal 是 Dog 的父类,Dog是Animal的子类型,子类型的属性比父类型更多,更具体。

  • 在类型系统中,属性更多的类型是子类型。
  • 在集合论中,属性更少的集合是子集。

也就是说,子类型是父类型的超集,而父类型是子类型的子集, 这是直觉上容易搞混的一点。

记住一个特征,子类型比父类型更加具体,这点很关键。

在联合类型中的运用

根据子类型的定义 'a' | 'b' | 'c''a' | 'b' 的子类型吗?

其实'a' | 'b' | 'c' 'a' | 'b' 的父类型。因为前者比后者更「宽泛」,后者比前者更「具体」。

type Parent = 'a' | 'b' | 'c'
type Son = 'a' | 'b'

let parent: Parent
let son: Son

parent = son // ✅ok
son = parent // ❌error! parent 有可能是 'c'

协变(Covariance)

协变比较好理解,子集必定有父集的属性,所以赋值是安全的。

interface SuperType {
    base: string;
}
interface SubType extends SuperType {
    addition: string;
};

// subtype compatibility
let superType: SuperType = { base: 'base' };
let subType: SubType = { base: 'myBase', addition: 'myAddition' };
superType = subType;

// Covariant
type Covariant<T> = T[];
let coSuperType: Covariant<SuperType> = [];
let coSubType: Covariant<SubType> = [];
coSuperType = coSubType;

逆变(Contravariance)

interface SuperType {
    base: string;
}
interface SubType extends SuperType {
    addition: string;
};
// Contravariant --strictFunctionTypes true
type Contravariant<T> = (p: T) => void;
let contraSuperType: Contravariant<SuperType> = function(p) {}
let contraSubType: Contravariant<SubType> = function(p) {}
contraSubType = contraSuperType;
//contraSuperType(p:SuperType) 此时的函数方法是contraSubType 如果含有SubType的addition的相关处理,就会出错。所以是不安全的
contraSuperType = contraSubType;

// Bivariant --strictFunctionTypes false
type Bivariant<T> = (p: T) => void;
let biSuperType: Bivariant<SuperType> = function(p) {}
let biSubType: Bivariant<SubType> = function(p) {}
// both are ok
biSubType = biSuperType;
biSuperType = biSubType;

// Invariant --strictFunctionTypes true
type Invariant<T> = { a: Covariant<T>, b: Contravariant<T> };
let inSuperType: Invariant<SuperType> = { a: coSuperType, b: contraSuperType }
let inSubType: Invariant<SubType> = { a: coSubType, b: contraSubType }
// both are not ok
inSubType = inSuperType;
inSuperType = inSubType;

双向协变(Bivariant)

双向协变(Bivariant):旨在支持常见的事件处理方案。

在 TypeScript 中, 参数类型是双向协变 的,也就是说既是协变又是逆变的,而这并不安全。

但是现在你可以在 TypeScript 2.6 版本中通过 --strictFunctionTypes 或 --strict 标记来修复这个问题。

所谓双向协变就是既可协变也可逆变

// 事件等级
interface Event {
  timestamp: number;
}
interface MouseEvent extends Event {
  x: number;
  y: number;
}
interface KeyEvent extends Event {
  keyCode: number;
}

// 简单的事件监听
enum EventType {
  Mouse,
  Keyboard
}
function addEventListener(eventType: EventType, handler: (n: Event) => void) {
  // ...
}

// 不安全,但是有用,常见。函数参数的比较是双向协变。
addEventListener(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));

// 在安全情景下的一种不好方案
addEventListener(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
addEventListener(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));

// 仍然不允许明确的错误,对完全不兼容的类型会强制检查
addEventListener(EventType.Mouse, (e: number) => console.log(e));

同样的,你也可以把 Array<Child> 赋值给 Array<Base> (协变),因为函数是兼容的。数组的协变需要所有的函数 Array<Child> 都能赋值给 Array<Base>,例如 push(t: Child) 能被赋值给 push(t: Base),这都可以通过函数参数双向协变实现。

下面的代码对于其他语言的开发者来说,可能会感到很困惑,因为他们认为是有错误的,可是 Typescript 不开启--strictFunctionTypes并不会报错:

interface Point2D {
  x: number;
  y: number;
}
interface Point3D {
  x: number;
  y: number;
  z: number;
}

let iTakePoint2D = (point: Point2D) => {};
let iTakePoint3D = (point: Point3D) => {};

iTakePoint3D = iTakePoint2D; // ok, 这是合理的
iTakePoint2D = iTakePoint3D; // ok,为什么?

有点意思

当你传入一个其他对象至索引签名时,JavaScript 会在得到结果之前会先调用 .toString 方法:

let obj = { toString() { console.log('toString called'); return 'Hello'; } };

let foo: any = {}; foo[obj] = 'World'; // toString called console.log(foo[obj]); // toString called, World console.log(foo['Hello']); // World

ES5上扩展 Error、Array、Map 内置函数

首先我们来看现象:

Babel Configuration

{
  "presets": [
    "es2015"
  ]
}
class FooError extends Error {}
console.log(new FooError() instanceof FooError); // false

没错,是 false。我眼看着它 new 出来,它就不是不是这个类型。

这样导致的结果是:ErrorArray 等子类将不再按预期工作。这是由于 ErrorArray 等的构造函数使用 ECMAScript6 中的 new.target 来调整原型链。但是,在 ECMAScript 5 中调用构造函数时,无法确保 new.target 的值。在其他一些低水平的编译器通常都有相同的限制。

TS 官方的建议的解决方案:

class FooError extends Error {
  constructor(m: string) {
    super(m);

    // Set the prototype explicitly.
    Object.setPrototypeOf(this, FooError.prototype);
  }
}

这段代码手动的将FooError加回了实例的原型链中。但它也存在一个很大的问题,就是所有的子类都必须再用这种方式手动设置原型链。这无疑是一个十分不清爽的解决办法。

而且,这个办法还有两个小问题:

class FooError extends Error {
  constructor(m: string) {
    super(m);
    Object.setPrototypeOf(this, FooError.prototype);
  }
}
class BarError extends FooError {
  constructor(m: string) {
    super(m);
    Object.setPrototypeOf(this, BarError.prototype);
  }
}
console.log(new BarError('I am BarError').stack);
// Error: I am BarError
//     at BarError.FooError [as constructor] (******)
//     at new BarError (******)
//     at Object.<anonymous> (******)
console.log(new Error('I am Error').stack);
// Error: I am Error
//    at Object.<anonymous> (******)

注意到:

  1. 相较于原生 Error,BarErrorFooErrorconstructor被加入到了 call stack 中,这无疑是不理想的,多层继承会给 debug 带来很多噪音。
  2. 尽管我们的 class 叫做BarError,但是 stack 的开头,异常的名字还是Error。这给我们在 debug 中寻找问题的源头造成了困难。

完美解决方案

// 使用 BaseError 当作 base Error class
class BaseError extends Error {
  constructor(message: string) {
    super(message);
    // 修复name
    this.name = new.target.name;
    // 修复 stack
    if (typeof (Error as any).captureStackTrace === 'function') {
      (Error as any).captureStackTrace(this, new.target);
    }
    // 修复原型链
    if (typeof Object.setPrototypeOf === 'function') {
      Object.setPrototypeOf(this, new.target.prototype);
    } else {
      (this as any).__proto__ = new.target.prototype;
    }
  }
}

// 使用例子
class MyError extends BaseError {
  // ...
}

动态查找

当导入路径不是相对路径时,模块解析将会模仿 Node 模块解析策略,下面我将给出一个简单例子:

  • 当你使用

    import * as foo from 'foo'
    

    将会按如下顺序查找模块:

    • ./node_modules/foo
    • ../node_modules/foo
    • ../../node_modules/foo

    对于大多数项目,我们建议使用外部模块和命名空间,来快速演示和移植旧的 JavaScript 代码。

编译选项


{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}

数字类型枚举与字符串类型

在我们继续深入学习枚举类型之前,先来看看它编译的 JavaScript 吧,以下是一个简单的 TypeScript 枚举类型:

enum Tristate {
  False,
  True,
  Unknown
}

其被编译成 JavaScript 后如下所示:

var Tristate;
(function(Tristate) {
  Tristate[(Tristate['False'] = 0)] = 'False';
  Tristate[(Tristate['True'] = 1)] = 'True';
  Tristate[(Tristate['Unknown'] = 2)] = 'Unknown';
})(Tristate || (Tristate = {}));

先让我们聚焦 Tristate[Tristate['False'] = 0] = 'False' 这行代码,其中 Tristate['False'] = 0 的意思是将 Tristate 对象里的 False 成员值设置为 0。注意,JavaScript 赋值运算符返回的值是被赋予的值(在此例子中是 0),因此下一次 JavaScript 运行时执行的代码是 Tristate[0] = 'False'。意味着你可以使用 Tristate 变量来把字符串枚举类型改造成一个数字或者是数字类型的枚举类型,如下所示:

enum Tristate {
  False,
  True,
  Unknown
}

console.log(Tristate[0]); // 'False'
console.log(Tristate['False']); // 0
console.log(Tristate[Tristate.False]); // 'False' because `Tristate.False == 0`

常量枚举

enum Tristate {
  False,
  True,
  Unknown
}

const lie = Tristate.False;

const lie = Tristate.False 会被编译成 JavaScript let lie = Tristate.False (是的,编译后与编译前,几乎相同)。这意味着在运行执行时,它将会查找变量 TristateTristate.False。在此处获得性能提升的一个小技巧是使用常量枚举:

const enum Tristate {
  False,
  True,
  Unknown
}

const lie = Tristate.False;

将会被编译成:

let lie = 0;

函数声明

在没有提供函数实现的情况下,有两种声明函数类型的方式:

type LongHand = {
  (a: number): number;
};

type ShortHand = (a: number) => number;

上面代码中的两个例子完全相同。但是,当你想使用函数重载时,只能用第一种方式:

type LongHandAllowsOverloadDeclarations = {
  (a: number): number;
  (a: string): string;
};


//只有实现了重载的声明式函数才能和FetchInstance匹配

interface FetchInstance {

  (a: string): string

  (a: number): number

}

function fetch(a: string): string

function fetch(a: number): number

function fetch(a: string | number): string | number {

  return 1

}

const fetchIns: FetchInstance = fetch

fetchIns(111)

它仅仅只能作为简单的箭头函数,你无法使用重载。如果想使用重载,你必须使用完整的 { (someArgs): someReturn } 的语法

并非每个接口都是很容易实现的

接口旨在声明 JavaScript 中可能存在的任意结构。

思考以下例子,可以使用 new 调用某些内容:

interface Crazy {
  new (): {
    hello: number;
  };
}

你可能会有下面这样的代码:

class CrazyClass implements Crazy {
  constructor() {
    return { hello: 123 };
  }
}

// Because
const crazy = new CrazyClass(); // crazy would be { hello:123 }

Freshness

为了能让检查对象字面量类型更容易,TypeScript 提供 「Freshness」 的概念(它也被称为更严格的对象字面量检查)用来确保对象字面量在结构上类型兼容。

Readonly

这有一个 Readonly 的映射类型,它接收一个泛型 T,用来把它的所有属性标记为只读类型:

type Foo = {
  bar: number;
  bas: number;
};

type FooReadonly = Readonly<Foo>;

const foo: Foo = { bar: 123, bas: 456 };
const fooReadonly: FooReadonly = { bar: 123, bas: 456 };

foo.bar = 456; // ok
fooReadonly.bar = 456; // Error: bar 属性只读

TypeScript 索引签名

JavaScript 在一个对象类型的索引签名上会隐式调用 toString 方法,而在 TypeScript 中,为防止初学者砸伤自己的脚(我总是看到 stackoverflow 上有很多 JavaScript 使用者都会这样。),它将会抛出一个错误。

const obj = {
  toString() {
    return 'Hello';
  }
};

const foo: any = {};

// ERROR: 索引签名必须为 string, number....
foo[obj] = 'World';

// FIX: TypeScript 强制你必须明确这么做:
foo[obj.toString()] = 'World';

强制用户必须明确的写出 toString() 的原因是:在对象上默认执行的 toString 方法是有害的。例如 v8 引擎上总是会返回 [object Object]

const obj = { message: 'Hello' };
let foo: any = {};

// ERROR: 索引签名必须为 string, number....
foo[obj] = 'World';

// 这里实际上就是你存储的地方
console.log(foo['[object Object]']); // World

当然,数字类型是被允许的,这是因为:

  • 需要对数组 / 元组完美的支持;
  • 即使你在上例中使用 number 类型的值来替代 objnumber 类型默认的 toString 方法实现的很友好(不是 [object Object])。

如下所示:

console.log((1).toString()); // 1
console.log((2).toString()); // 2

因此,我们有以下结论:

TIP

TypeScript 的索引签名必须是 string 或者 number

设计模式:索引签名的嵌套

TIP

添加索引签名时,需要考虑的 API。

在 JavaScript 社区你将会见到很多滥用索引签名的 API。如 JavaScript 库中使用 CSS 的常见模式:

interface NestedCSS {
  color?: string; // strictNullChecks=false 时索引签名可为 undefined
  [selector: string]: string | NestedCSS;
}

const example: NestedCSS = {
  color: 'red',
  '.subclass': {
    color: 'blue'
  }
};

尽量不要使用这种把字符串索引签名与有效变量混合使用。如果属性名称中有拼写错误,这个错误不会被捕获到:

const failsSilently: NestedCSS = {
  colour: 'red' // 'colour' 不会被捕捉到错误
};

取而代之,我们把索引签名分离到自己的属性里,如命名为 nest(或者 childrensubnodes 等):

interface NestedCSS {
  color?: string;
  nest?: {
    [selector: string]: NestedCSS;
  };
}

const example: NestedCSS = {
  color: 'red',
  nest: {
    '.subclass': {
      color: 'blue'
    }
  }
}

const failsSliently: NestedCSS {
  colour: 'red'  // TS Error: 未知属性 'colour'
}

ThisType

通过 ThisType 我们可以在对象字面量中键入 this,并提供通过上下文类型控制 this 类型的便捷方式。它只有在 --noImplicitThis 的选项下才有效。

现在,在对象字面量方法中的 this 类型,将由以下决定:

  • 如果这个方法显式指定了 this 参数,那么 this 具有该参数的类型。(下例子中 bar
  • 否则,如果方法由带 this 参数的签名进行上下文键入,那么 this 具有该参数的类型。(下例子中 foo
  • 否则,如果 --noImplicitThis 选项已经启用,并且对象字面量中包含由 ThisType<T> 键入的上下文类型,那么 this 的类型为 T
  • 否则,如果 --noImplicitThis 选项已经启用,并且对象字面量中不包含由 ThisType<T> 键入的上下文类型,那么 this 的类型为该上下文类型。
  • 否则,如果 --noImplicitThis 选项已经启用,this 具有该对象字面量的类型。
  • 否则,this 的类型为 any

一些例子:

// Compile with --noImplicitThis

type Point = {
  x: number;
  y: number;
  moveBy(dx: number, dy: number): void;
};

let p: Point = {
  x: 10,
  y: 20,
  moveBy(dx, dy) {
    this.x += dx; // this has type Point
    this.y += dy; // this has type Point
  }
};

let foo = {
  x: 'hello',
  f(n: number) {
    this; // { x: string, f(n: number): void }
  }
};

let bar = {
  x: 'hello',
  f(this: { message: string }) {
    this; // { message: string }
  }
};

对象字面量的惰性初始化

折中的解决方案

当然,总是用 any 肯定是不好的,因为这样做其实是在想办法绕开 TypeScript 的类型检查。那么,折中的方案就是创建 interface,这样的好处在于:

  • 方便撰写类型文档
  • TypeScript 会参与类型检查,确保类型安全

请看以下的示例:

interface Foo {
  bar: number;
  bas: string;
}

let foo = {} as Foo;
foo.bar = 123;
foo.bas = 'Hello World';

使用 interface 可以确保类型安全,比如这种情况:

interface Foo {
  bar: number;
  bas: string;
}

let foo = {} as Foo;
foo.bar = 123;
foo.bas = 'Hello World';

// 然后我们尝试这样做:
foo.bar = 'Hello Stranger'; // 错误:你可能把 `bas` 写成了 `bar`,不能为数字类型的属性赋值字符串

export default 被认为是有害的

假如你有一个包含以下内容的 foo.ts 文件:

class Foo {}

export default Foo;

你可能会使用 ES6 语法导入它(在 bar.ts 里):

import Foo from './foo';

这存在一些可维护性的问题:

  • 如果你在 foo.ts 里重构 Foo,在 bar.ts 文件中,它将不会被重新命名;
  • 如果你最终需要从 foo.ts 文件中导出更多有用的信息(在你的很多文件中都存在这种情景),那么你必须兼顾导入语法。

由于这些原因,我推荐在导入时使用简单的 export 与解构的形式,如 foo.ts

export class Foo {}

接着:

import { Foo } from './Foo';

函数参数

如果你有一个含有很多参数或者相同类型参数的函数,那么你可能需要考虑将函数改为接收对象的形式:

如下一个函数:

function foo(flagA: boolean, flagB: boolean) {
  // 函数主体
}

像这样的函数,你可能会很容易错误的调用它,如 foo(flagB, flagA),并且你并不会从编译器得到想要的帮助。

你可以将函数变为接收对象的形式:

function foo(config: { flagA: boolean; flagB: boolean }) {
  const { flagA, flagB } = config;
}

现在,函数将会被 foo({ flagA, flagB }) 的形式调用,这样有利于发现错误及代码审查。

Truthy

JavaScript 有一个 truthy 概念,即在某些场景下会被推断为 true,例如除 0 以外的任何数字:

if (123) {
  // 将会被推断出 `true`
  console.log('Any number other than 0 is truthy');
}

你可以用下表来做参考:

Variable TypeWhen it is falsyWhen it is truthy
booleanfalsetrue
string' ' (empty string)any other string
number0 NaNany other number
nullalwaysnever
Any other Object including empty ones like {},[]neveralways

通过操作符 !!,你可以很容易的将某些值转化为布尔类型的值,例如:!!foo,它使用了两次 !,第一个 ! 用来将其(在这里是 foo)转换为布尔值,但是这一操作取得的是其取反后的值,第二个取反时,能得到真正的布尔值。

这在很多地方都可以看到:

// Direct variables
const hasName = !!name;

// As members of objects
const someObj = {
  hasName: !!name
};

// ReactJS
{
  !!someName && <div>{someName}</div>;
}

什么是名义上的类

这两段代码该如何解释:

class Alpha {
  x: number;
}
class Bravo {
  x: number;
}
class Charlie {
  private x: number;
}
class Delta {
  private x: number;
}

let a = new Alpha(),
  b = new Bravo(),
  c = new Charlie(),
  d = new Delta();

a = b; // OK
c = d; // Error

在 TypeScript 中,类进行结构上的比较,有一个例外是对于 privateprotected 的成员。当一个成员是 private 或者 protected 时,它们必须来自同一个声明,才能被视为与另一个 private 或者 protected 的成员相同。

Bar 是一个 class 时,Bartypeof Bar 有什么区别?

class MyClass {
  someMethod() {}
}
var x: MyClass;
// Cannot assign 'typeof MyClass' to MyClass? Huh?
x = MyClass;

在 JavaScript 中,类仅仅是个函数,这点很重要。我们将类对象本身 -- MyClass 的值,作为是构造函数。当一个构造函数被 new 调用时,我们得到一个对象,它是该类的实例。

因此,当我们定义一个类时,实际上,我们定义了两个不同的类型。

第一个是由类的名字推导而来,在这个例子中是 MyClass。这个是类实例的类型,它定义了类的实例具有的属性和方法,它是一个通过调用类的构造函数来返回的类型。

第二个类型是一个匿名的类型,它是构造函数具有的类型。它包含一个返回类实例的构造函数签名(可以使用 new 调用),同时,它也包含类中可能含有的 static 属性和方法。它也通常被称为「静态方面」,因为它包含那些静态成员(以及作为类的构造函数)。我们可以用 typeof 来引用此类型。

当在类型位置使用 typeof 操作符时,描述了表达式的类型。因此 typeof MyClass 是指 MyClass 的类型。

构造函数

当你在TypeScript里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的 实例的类型。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

这里,我们写了 let greeter: Greeter,意思是 Greeter类的实例的类型是 Greeter。 这对于用过其它面向对象语言的程序员来讲已经是老习惯了。

我们也创建了一个叫做 构造函数的值。 这个函数会在我们使用 new创建类实例的时候被调用。 下面我们来看看,上面的代码被编译成JavaScript后是什么样子的:

let Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();

let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

上面的代码里, let Greeter将被赋值为构造函数。 当我们调用 new并执行了这个函数后,便会得到一个类的实例。 这个构造函数也包含了类的所有静态属性。 换个角度说,我们可以认为类具有 实例部分静态部分这两个部分。

让我们稍微改写一下这个例子,看看它们之间的区别:

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    greet() {
        if (this.greeting) {
            return "Hello, " + this.greeting;
        }
        else {
            return Greeter.standardGreeting;
        }
    }
}

let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

这个例子里, greeter1与之前看到的一样。 我们实例化 Greeter类,并使用这个对象。 与我们之前看到的一样。

再之后,我们直接使用类。 我们创建了一个叫做 greeterMaker的变量。 这个变量保存了这个类或者说保存了类构造函数。 然后我们使用 typeof Greeter,意思是取Greeter类的类型,而不是实例的类型。 或者更确切的说,"告诉我 Greeter标识符的类型",也就是构造函数的类型。 这个类型包含了类的所有静态成员和构造函数。 之后,就和前面一样,我们在 greeterMaker上使用 new,创建 Greeter的实例。

为什么不要在泛型函数中写 typeof Tnew T, 或者 instanceof T

function doSomething<T>(x: T) {
  // Can't find name T?
  let xType = typeof T;
  let y = new xType();
  // Same here?
  if(someVar instanceof typeof T) {

  }
  // How do I instantiate?
  let z = new T();
}

泛型在编译期间被删除,这意味着在 doSomething 运行时没有值为 T 。这里人们试图表达的正常模式是将类的构造函数用于工厂或运行时类型检查。。在这两种情况下,使用构造签名并将其作为参数提供是正确的:

function create<T>(ctor: { new(): T }) {
  return new ctor();
}
var c = create(MyClass); // c: MyClass

function isReallyInstanceOf<T>(ctor: { new(...args: any[]): T }, obj: T) {
  return obj instanceof ctor;
}

范型函数的错误示范

const foo = <T>(x: T) => T; // Error: T 标签没有关闭 

解决办法:在泛型参数里使用 extends 来提示编译器,这是个泛型:

const foo = <T extends {}>(x: T) => x;

Exported variable [name] has or is using private name [name] 是什么错误?

当你使用 --declarartion 编译选项的时候,可能会出现这个错误,因为编译器试图生成与你定义模块完全匹配的声明文件:

假设你有这样一段代码:

/// MyFile.ts
class Test {
  // ... other members ....
  constructor(public parent: Test) {}
}

export let t = new Test('some thing');

为了生成声明文件,编译器必须为 t 写一个类型:

/// MyFile.d.ts, auto-generated
export let t: ___fill in the blank___;

成员 t 有类型 Test,但是类型 Test 并不是可见的,因为它没有导出,因此我们不能写 t: Test

在这个非常简单的例子里,我们可以用一个对象字面量重写 Test's 的形状。但是对于绝大多数情况,这并不能正常工作。如代码里所写,Test 的形状是自引用的,不能重写为匿名函数。如果 Test 有任何私有或受保护的成员,这同样也不能正常工作。因此,与其让你通过编写一个真实的类来获得 65% 的成功而后开始抛出错误,我们仅仅是在一开始的时候就抛出错误(你以后会发现)并为你省去不必要的麻烦。

为了避免这些错误:

  • 导出相关类型中使用的声明
  • 当编写声明的时候,显示的为编译器指定类型注解

参考

juejin.cn/post/684490…

  • ES5上扩展 Error、Array、Map 内置函数

stackoverflow.com/questions/1…

zhuanlan.zhihu.com/p/113019880