「TypeScript」 开发手册梳理(下)

532 阅读37分钟

基于之前【TypeScript历险记】文章,结合 TypeScript 官网开发手册指南,重新简要概括在学习过程中的重点难点

致力于以下几点:

  1. 一文扫清 TypeScript 官网开发手册指南。
  2. 内容简洁、重点。
  3. 排版清晰、一目了然。
  4. 重构语句、语句使之顺畅。

因字数限制,分为上下两篇。

跳转(上)

声明合并

  • 声明合并是指编译器将针对同一个名字的两个独立声明合并为单一声明,合并后的声明同时拥有原先两个声明的特性,并且任何数量的声明都可被合并。

这将有助于操作现有的 JavaScript 代码,也会有助于理解更多高级抽象的概念。

基础概念

  • TypeScript中的声明会创建以下三种实体之一:命名空间,类型或值。
    • 创建命名空间的声明:新建一个命名空间,它包含了用(.)符号来访问时使用的名字。
    • 创建类型的声明:用声明的模型创建一个类型并绑定到给定的名字上。
    • 创建值的声明:会创建在JavaScript输出中看到的值。
Declaration TypeNamespaceTypeValue
NamespaceXX
ClassXX
EnumXX
InterfaceX
Type AliasX
FunctionX
VariableX

理解每个声明创建了什么,有助于理解当声明合并时有哪些东西被合并了。

合并接口

  • 从根本上说,合并的机制是把双方的成员放到一个同名的接口里。
interface Box {
    height: number;
    width: number;
}
interface Box {
    scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10};
  • 接口的非函数的成员应该是唯一的,如果不是唯一的,那么必须是相同类型。

如果两个接口中同时声明同名的非函数成员且类型不同,则编译器报错。

  • 函数成员,每个同名函数声明都会被当成这个函数的一个重载。

后面的接口具有更高的优先级。

interface Cloner {
    clone(animal: Animal): Animal;
}
interface Cloner {
    clone(animal: Sheep): Sheep;
}
interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
}
// 等同合并如下(注意合并重载顺序)
interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
    clone(animal: Sheep): Sheep;
    clone(animal: Animal): Animal;
}
  • 如果函数签名有一个参数的类型是单一的字符串字面量,那么它将会被提升到重载列表的最顶端(不遵循重载优先级规则)。
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;
}
// 合并如下
interface Document {
    createElement(tagName: "canvas"): HTMLCanvasElement;
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
    createElement(tagName: string): HTMLElement;
    createElement(tagName: any): Element;
}

合并命名空间

  • 命名空间将模块导出的同名接口进行合并,构成单一命名空间,内含合并后的接口。
  • 如果当前已经存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里。
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 { }
}
  • 非导出成员仅在其原有的(合并前的)命名空间内可见。

这就是说合并之后,从其它命名空间合并进来的成员无法访问非导出成员。

namespace Animal {
    let haveMuscles = true;
    export function animalsHaveMuscles() {
        return haveMuscles;
    }
}

namespace Animal {
    export function doAnimalsHaveMuscles() {
        return haveMuscles;  // Error, 无法访问非导出成员
    }
}

命名空间与类/函数/枚举类型合并

  • 只要命名空间的定义符合将要合并类型的定义,则可以与其它类型的声明进行合并,并且合并结果包含两者的声明类型。

TypeScript使用这个功能去实现一些JavaScript里的设计模式。

  • 合并命名空间和类:可以表示为内部类
class Album {
    label: Album.AlbumLabel;
}
namespace Album {
    export class AlbumLabel { }
}

合并结果是一个类并带有一个内部类,并且可以使用命名空间为类增加一些静态属性。

  • 合并命名空间和函数:扩展增加属性并保证类型安全。
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"));
  • 合并命名空间和枚举类型:扩展枚举类型
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;
        }
    }
}

合并类

  • 类不能与其它类或变量合并。

了解如何模仿类的合并,请参考 TypeScript的混入

模块扩展

  • 针对 JavaScript 不支持合并,可以利用为导入的对象打补丁以更新它们。
// observable.ts
export class Observable<T> {
    // ... implementation left as an exercise for the reader ...
}

// map.ts
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
    // ... another exercise for the reader
}

上述示例中编译器对 Observable.prototype.map一无所知。因此,可以使用扩展模块来进行说明。

// map.ts
import { Observable } from "./observable";
// 模块扩展
declare module "./observable" {
    interface Observable<T> {
      	// 说明 `Observable.prototype.map`
        map<U>(f: (x: T) => U): Observable<U>;
    }
}
Observable.prototype.map = function (f) {
    // ... another exercise for the reader
}


// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());

当这些声明在扩展中合并时,就好像在原始位置被声明了一样。 但是,不能在扩展中声明新的顶级声明,仅可以扩展模块中已经存在的声明。

  • 可以在模块内部添加声明到全局作用域中。
// observable.ts
export class Observable<T> {
    // ... still no implementation ...
}

declare global {
    interface Array<T> {
        toObservable(): Observable<T>;
    }
}

Array.prototype.toObservable = function () {
    // ...
}

全局扩展与模块扩展的行为和限制是相同的。

JSX

JSX是一种嵌入式的类似XML的语法,可以被转换成合法的JavaScript,同时 JSX 因 React 框架而流行,TypeScript支持内嵌,类型检查以及将JSX直接编译为JavaScript。

关于 JSX 更多可看《React 为什么是 JSX 而不是 Templates 》

  • TypeScript 使用 JSX 语法需做两件事
    1. 文件更新 .tsx 扩展名
    2. 启用 jsx 选项
  • TypeScript具有三种JSX模式:preservereactreact-native,这些模式只在代码生成阶段起作用,在类型检查并不受影响。
    • 可以通过在命令行里使用--jsx标记或tsconfig.json里的选项来指定模式。
模式输入输出输出文件扩展名
preserve<div /><div />.jsx
react<div />React.createElement("div").js
react-native<div /><div />.js

注意:React标识符是写死的硬编码,所以你必须保证React(大写的R)是可用的。

as 操作符

  • 在结合 JSX 的语法后将带来解析上的困难,TypeScript在.tsx文件里禁用了使用尖括号的类型断言。因此,使用另一个类型断言操作符:as
var foo = bar as foo;

as操作符在.ts.tsx里都可用,并且与尖括号类型断言行为是等价的。

类型检查

  • 为了理解JSX的类型检查,必须先理解固有元素与基于值的元素之间的区别。
    • 对于React,固有元素会生成字符串(React.createElement("div")),然而由你自定义的组件却不会生成(React.createElement(MyComponent))。
    • 传入 JSX 元素里的属性类型的查找方式不同。
      • 固有元素属性本身就支持查找
      • 自定义的组件会自己去指定它们具有哪个属性。

TypeScript使用与React相同的规范 来区别它们。 固有元素总是以一个小写字母开头,基于值的元素总是以一个大写字母开头。

  • 固有元素使用特殊的接口JSX.IntrinsicElements来查找。

    • 若没有指定该特殊接口,默认会全部通过,不对固有元素进行类型检查。
    • 当指定该特殊接口存在,固有元素的名字需要在JSX.IntrinsicElements接口的属性里查找,否则报错。
    declare namespace JSX {
        interface IntrinsicElements {
            foo: any
        }
    }
    
    <foo />; // 正确
    <bar />; // 错误,没在 JSX.IntrinsicElements 里指定
      
    
    // 也可以指定一个用来捕获所有字符串索引
    declare namespace JSX {
        interface IntrinsicElements {
            [elemName: string]: any;
        }
    }
    
  • 基于值的元素会简单在它所在的作用域里按标识符查找。

import MyComponent from "./myComponent";
                                
<MyComponent />; // 正确
<SomeOtherComponent />; // 错误
  • 值的元素有两种方式可以定义。
    1. 无状态函数组件 (SFC)
    2. 类组件

这两种基于值的元素在 JSX 表达式里无法区分,因此 TypeScript 首先尝试将表达式做为无状态函数组件进行解析。如果解析失败,继续尝试以类组件的形式进行解析。如果依旧失败,那么将输出一个错误。

  • 无状态函数组件,被定义成JavaScript函数,其第一个参数是props对象。 TypeScript会强制它的返回值可以赋值给JSX.Element
interface FooProp {
    name: string;
    X: number;
    Y: number;
}

declare function AnotherComponent(prop: {name: string});
function ComponentFoo(prop: FooProp) {
    return <AnotherComponent name={prop.name} />;
}

const Button = (prop: {value: string}, context: { color: string }) => <button>

也可以利用函数重载

interface ClickableProps {
    children: JSX.Element[] | JSX.Element
}
interface HomeProps extends ClickableProps {
    home: JSX.Element;
}
interface SideProps extends ClickableProps {
    side: JSX.Element | string;
}

function MainButton(prop: HomeProps): JSX.Element;
function MainButton(prop: SideProps): JSX.Element {
    ...
}
  • 定义类组件类型需弄懂两个新的术语:元素类的类型元素实例的类型
    • 假设 <Expr /> 元素类的类型为 Expr 的类型。
      • 如果是 ES6 类,那么类类型就是类的构造函数和静态部分。
      • 如果是工厂函数,类类型为这个函数。
    • 当建立起了类类型,实例类型由类构造器或调用签名的返回值的联合构成。
      • 如果是 ES6 类,实例类型为该类的实例类型。
      • 如果是工厂函数,实例类型为该函数返回值类型。
class MyComponent {
    render() {}
}
// 使用构造签名
var myComponent = new MyComponent();
// 元素类的类型 => MyComponent
// 元素实例的类型 => { render: () => void }

function MyFactoryFunction() {
    return {
    	render: () => {}
    }
}
// 使用调用签名
var myComponent = MyFactoryFunction();
// 元素类的类型 => MyFactoryFunction
// 元素实例的类型 => { render: () => void }

元素的实例类型必须赋值给JSX.ElementClass或抛出一个错误。

默认的JSX.ElementClass{},但是可以被扩展用来限制 JSX 的类型以符合相应的接口。

declare namespace JSX {
    interface ElementClass {
    	render: any;
    }
}

class MyComponent {
    render() {}
}
function MyFactoryFunction() {
    return { render: () => {} }
}
<MyComponent />; // 正确
<MyFactoryFunction />; // 正确

class NotAValidComponent {}
function NotAValidFactoryFunction() {
    return {};
}
<NotAValidComponent />; // 错误,未定义类型
<NotAValidFactoryFunction />; // 错误,未定义类型

属性类型检查

  • 元素属性类型用于的JSX里进行属性的类型检查。第一步是确定元素属性类型,是固有元素还是基于值的元素。

    • 对于固有元素,将检查JSX.IntrinsicElements属性的类型。
    declare namespace JSX {
        interface IntrinsicElements {
        	foo: { bar?: boolean }
        }
    }
    
    // `foo`的元素属性类型为`{bar?: boolean}`
    <foo bar />;
    
    • 对于基于值的元素,取决于先前确定的在元素实例类型上的某个属性的类型。
      • 该使用哪个属性来确定类型取决于使用单一的属性来定义JSX.ElementAttributesProperty,该属性名也可被使用。
      • TypeScript 2.8,如果未指定JSX.ElementAttributesProperty,那么将使用类元素构造函数或 SFC 调用的第一个参数的类型。
    declare namespace JSX {
        interface ElementAttributesProperty {
        	props; // 指定用来使用的属性名
        }
    }
    
    class MyComponent {
        // 在元素实例类型上指定属性
        props: {
        	foo?: string;
        }
    }
    
    // `MyComponent`的元素属性类型为`{foo?: string}`
    <MyComponent foo="bar" />
    
  • 元素属性类型检查支持可选属性和必须属性。

declare namespace JSX {
    interface IntrinsicElements {
    	foo: { requiredProp: string; optionalProp?: number }
    }
}

<foo requiredProp="bar" />; // 正确
<foo requiredProp="bar" optionalProp={0} />; // 正确
<foo />; // 错误, 缺少 requiredProp
<foo requiredProp={0} />; // 错误, requiredProp 应该是字符串
<foo requiredProp="bar" unknownProp />; // 错误, unknownProp 不存在
<foo requiredProp="bar" some-unknown-prop />; // 正确, `some-unknown-prop`不是个合法的标识符

如果一个属性名不是个合法的JS标识符(像data-*属性),并且它没出现在元素属性类型里时不会当做一个错误,而是字符串。

  • JSX还会使用JSX.IntrinsicAttributes接口来指定额外的属性,这些额外的属性通常不会被组件的 props 或 arguments 使用(比如React里的key)。

    • 在 React 里,它用来允许Ref<T>类型上的ref属性。通常来讲,这些接口上的所有属性都是可选的,除非你想要用户在每个JSX标签上都提供一些属性。
  • JSX.IntrinsicClassAttributes<T>泛型类型也可以用来做同样的事情(这里泛型参数表示类实例类型。)。

  • 也可使用扩展操作符 ...

var props = { requiredProp: 'bar' };
<foo {...props} />; // 正确

var badProps = {};
<foo {...badProps} />; // 错误

子孙类型检查

  • 从TypeScript 2.3开始,引入了children类型检查。同属性检查一样,可以利用单一的属性JSX.ElementChildrenAttribute来决定children名。

children元素属性(attribute)类型的一个特殊属性(property),子JSXExpression将会被插入到属性里。

declare namespace JSX {
    interface ElementChildrenAttribute {
    	children: {};  // specify children name to use
    }
}
  • 如不特殊指定子孙的类型,将使用React typings里的默认类型。
// 默认使用该类型
interface PropsType {
    children: JSX.Element
    name: string
}

class Component extends React.Component<PropsType, {}> {
    render() {
        return (
            <h2>
            {this.props.children}
            </h2>
        )
    }
}

// OK
<Component>
    <h1>Hello World</h1>
</Component>

// Error: children is of type JSX.Element not array of JSX.Element
<Component>
    <h1>Hello World</h1>
    <h2>Hello World</h2>
</Component>

// Error: children is of type JSX.Element not array of JSX.Element or string.
<Component>
    <h1>Hello</h1>
    World
</Component>

JSX 结果类型

  • JSX 表达式结果的类型默认为any
  • 可以通过指定JSX.Element接口,自定义结果类型。

该类型是一个黑盒,不能够从接口里检索元素,属性或JSX的子元素的类型信息。

嵌入的表达式

  • JSX允许使用{ }标签来内嵌表达式。
var a = <div>
    {['foo', 'bar'].map(function (i) { return <span>{i / 2}</span>; })}
</div>

React整合

  • 使用 React 类型)定义,可一起使用JSX和React,其类型声明定义了JSX合适命名空间来使用React。
/// <reference path="react.d.ts" />
interface Props {
    foo: string;
}

class MyComponent extends React.Component<Props, {}> {
    render() {
    return <span>{this.props.foo}</span>
    }
}

<MyComponent foo="bar" />; // 正确
<MyComponent foo={0} />; // 错误

工厂函数

  • 可以使用jsxFactory命令行选项,或在每个文件上内联地设置@jsx注释指令,进行 jsx: react编译选项使用的工厂函数配置。

比如,给createElement设置jsxFactory<div />会使用createElement("div")来生成,而不是React.createElement("div")

  • 注释指令可以像下面这样使用(在TypeScript 2.8里):
import preact = require("preact");
/* @jsx preact.h */
const x = <div />;

生成:

const preact = require("preact");
const x = preact.h("div", null);
  • 工厂函数的选择同样会影响JSX命名空间的查找(类型检查)。
    • 如果工厂函数默认使用React.createElement定义,编译器会先检查React.JSX,之后才检查全局的JSX
    • 如果工厂函数定义为h,那么在检查全局的JSX之前先检查h.JSX

装饰器

  • 当 TypeScript 和 ES6 里引入了类,在一些场景下需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)在类的声明及成员上通过元编程语法添加标注提供了一种方式。

Javascript里的装饰器目前处在 建议征集的第二阶段,但在TypeScript里已做为一项实验性特性予以支持。

装饰器是一项实验性特性,在未来的版本中可能会发生改变。

  • 若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:
    • 命令行: tsc --target ES5 --experimentalDecorators
    • tsconfig.json:
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

基础

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

例如,有一个@sealed装饰器,我们会这样定义sealed函数:

function sealed(target) {
    // do something with "target" ...
}

装饰器工厂

  • 装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用,应用到一个声明上。
function color(value: string) { // 这是一个装饰器工厂
    return function (target) { //  这是装饰器
        // do something with "target" and "value"...
    }
}

装饰器组合

  • 多个装饰器可以同时应用到一个声明上,求值方式与复合函数相似。:

    • 书写在同一行上:@f @g x
    • 书写在多行上:
    @f
    @g
    x
    
    // 当前模型复合结果等同于 f(g(x))。
    
  • 在 TypeScript 里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:

    1. 由上至下依次对装饰器表达式求值。
    2. 求值的结果会被当作函数,由下至上依次调用。

如果我们使用装饰器工厂的话,可以通过下面的例子来观察它们求值的顺序:

function f() {
    console.log("f(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("f(): called");
    }
}

function g() {
    console.log("g(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("g(): called");
    }
}

class C {
    @f()
    @g()
    method() {}
}

在控制台里会打印出如下结果:

// 由上至下对装饰器表达式求值
f(): evaluated
g(): evaluated
// 由下至上对值结果进行函数调用
g(): called
f(): called

类似栈的运行。

装饰器求值

  • 类中不同声明上的装饰器将按以下规定的顺序应用:
    1. 参数装饰器,然后依次是方法装饰器访问符装饰器,或属性装饰器应用到每个实例成员
    2. 参数装饰器,然后依次是方法装饰器访问符装饰器,或属性装饰器应用到每个静态成员
    3. 参数装饰器应用到构造函数
    4. 类装饰器应用到

类装饰器

  • 类装饰器在类声明之前被声明(紧靠着类声明)。
  • 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。
  • 类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)。
  • 类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
  • 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

如果要返回一个新的构造函数,则必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中不会为你做这些。

下面是使用类装饰器(@sealed)的例子,应用在Greeter类:

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

可以这样定义@sealed装饰器:

// 当@sealed被执行的时候,它将利用 Object.seal 密封此类的构造函数和原型。
function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

下面是一个重载构造函数的例子。

function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override";
    }
}

@classDecorator
class Greeter {
    property = "property";
    hello: string;
    constructor(m: string) {
        this.hello = m;
    }
}

console.log(new Greeter("world"));

方法装饰器

  • 方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。

  • 被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。

  • 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。

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

    1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
    2. 成员的名字。
    3. 成员的属性描述符

如果代码输出目标版本小于ES5属性描述符将会是undefined

  • 如果方法装饰器返回一个值,它会被用作方法的属性描述符

如果代码输出目标版本小于ES5返回值会被忽略。

下面是一个方法装饰器(@enumerable)的例子,应用于Greeter类的方法上:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

我们可以用下面的函数声明来定义@enumerable装饰器:

function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}

这里的@enumerable(false)是一个装饰器工厂

当装饰器 @enumerable(false)被调用时,它会修改属性描述符的enumerable属性。

访问器装饰器

  • 访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。
  • 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。
  • 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。

TypeScript 不允许同时装饰一个成员的getset访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。

这是因为,在装饰器应用于一个属性描述符时,它联合了getset访问器,而不是分开声明的。

  • 访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
    1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
    2. 成员的名字。
    3. 成员的属性描述符

如果代码输出目标版本小于ES5Property Descriptor将会是undefined

  • 如果访问器装饰器返回一个值,它会被用作方法的属性描述符

如果代码输出目标版本小于ES5返回值会被忽略。

下面是使用了访问器装饰器(@configurable)的例子,应用于Point类的成员上:

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

    @configurable(false)
    get x() { return this._x; }

    @configurable(false)
    get y() { return this._y; }
}

我们可以通过如下函数声明来定义@configurable装饰器:

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

属性装饰器

  • 属性装饰器声明在一个属性声明之前(紧靠着属性声明)。

  • 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。

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

    1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
    2. 成员的名字。

属性描述符不会做为参数传入属性装饰器,这与 TypeScript 是如何初始化属性装饰器的有关。

目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法,返回值也会被忽略。

因此,属性描述符只能用来监视类中是否声明了某个名字的属性。

我们可以用它来记录这个属性的元数据,如下例所示:

class Greeter {
    @format("Hello, %s")
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        let formatString = getFormat(this, "greeting");
        return formatString.replace("%s", this.greeting);
    }
}

然后定义@format装饰器和getFormat函数:

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

这个@format("Hello, %s")装饰器是个 装饰器工厂

@format("Hello, %s")被调用时,通过reflect-metadata库里的Reflect.metadata函数,添加一条这个属性的元数据。

getFormat被调用时,它读取格式的元数据。

这个例子需要使用reflect-metadata库。 查看 元数据了解reflect-metadata库更详细的信息。

参数装饰器

  • 参数装饰器声明在一个参数声明之前(紧靠着参数声明)。

  • 参数装饰器应用于类构造函数或方法声明。

  • 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。

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

    1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
    2. 成员的名字。
    3. 参数在函数参数列表中的索引。
  • 参数装饰器只能用来监视一个方法的参数是否被传入。

  • 参数装饰器的返回值会被忽略。

下例定义了参数装饰器(@required)并应用于Greeter类方法的一个参数:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @validate
    greet(@required name: string) {
        return "Hello " + name + ", " + this.greeting;
    }
}

使用下面的函数定义 @required@validate 装饰器:

import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

// 参数装饰器
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(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
    let method = descriptor.value;
    descriptor.value = function () {
        let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
        if (requiredParameters) {
            for (let parameterIndex of requiredParameters) {
                if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
                    throw new Error("Missing required argument.");
                }
            }
        }

        return method.apply(this, arguments);
    }
}

@required装饰器添加了元数据实体把参数标记为必需的。

@validate装饰器把greet方法包裹在一个函数里在调用原先的函数前验证函数参数。

这个例子使用了reflect-metadata库。 查看 元数据了解reflect-metadata库的更多信息。

元数据

  • 上述例子使用了reflect-metadata库来支持实验性的metadata API。 这个库还不是ECMAScript (JavaScript)标准的一部分。

当装饰器被ECMAScript官方标准采纳后,这些扩展也将被推荐给ECMAScript以采纳。

  • 你可以通过npm安装这个库:
npm i reflect-metadata --save
  • TypeScript支持为带有装饰器的声明生成元数据,需要在命令行或 tsconfig.json里启用emitDecoratorMetadata编译器选项。

    • Command Line:tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
    • tsconfig.json:
    {
        "compilerOptions": {
            "target": "ES5",
            "experimentalDecorators": true,
            "emitDecoratorMetadata": true
        }
    }
    
  • 启用后,只要reflect-metadata库被引入了,设计阶段添加的类型信息可以在运行时使用。

import "reflect-metadata";

class Point {
    x: number;
    y: number;
}

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}

function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
    let set = descriptor.set;
    descriptor.set = function (value: T) {
        let type = Reflect.getMetadata("design:type", target, propertyKey);
        if (!(value instanceof type)) {
            throw new TypeError("Invalid type.");
        }
        set(value);
    }
}

TypeScript编译器可以通过@Reflect.metadata装饰器注入设计阶段的类型信息。

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    @Reflect.metadata("design:type", Point)
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    @Reflect.metadata("design:type", Point)
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}

装饰器元数据是个实验性的特性并且可能在以后的版本中发生破坏性的改变(breaking changes)。

Mixins

  • 除了传统的面向对象继承方式,还流行一种通过可重用组件创建类的方式(联合另一个简单类的代码)。

  • 示例在 TypeScript 里使用 Mixins(混入)

定义两个 mixins 类,每个类都只定义了一个特定的行为或功能。

// Disposable Mixin
class Disposable {
    isDisposed: boolean;
    dispose() {
        this.isDisposed = true;
    }

}
// Activatable Mixin
class Activatable {
    isActive: boolean;
    activate() {
        this.isActive = true;
    }
    deactivate() {
        this.isActive = false;
    }
}

创建类,使用 implements 实现而非 extends 继承两个 mixins,把类当作接口来仅使用 DisposableActivatable 的类型,为混入的属性方法创建占位符而非实现。

class SmartObject implements Disposable, Activatable {
    constructor() {
        setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
    }
    interact() {
        this.activate();
    }
    // Disposable
    isDisposed: boolean = false;
    dispose: () => void;
    // Activatable
    isActive: boolean = false;
    activate: () => void;
    deactivate: () => void;
}

创建帮助混入操作函数,完成实现方法属性部分。

// 混入操作
applyMixins(SmartObject, [Disposable, Activatable]);

let smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);

// 应用混入函数
function applyMixins(derivedCtor: any, baseCtors: any[]) {
  // 遍历 mixins 上的所有属性,并复制到目标上,把之前的占位属性替换成真正的实现代码。
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            derivedCtor.prototype[name] = baseCtor.prototype[name];
        });
    });
}

三斜线指令

  • 三斜线指令是包含单个XML标签的单行注释,内容会做为编译器指令使用。

  • 三斜线指令可放在包含它的文件的最顶端。

  • 一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。

如果它们出现在一个语句或声明之后,那么它们会被当做普通的单行注释,并且不具有特殊的涵义。

  • /// <reference path="..." />指令是三斜线指令中最常见的一种,用于声明文件间的依赖,告诉编译器在编译过程中要引入的额外的文件。

  • 当使用--out--outFile时,它也可以做为调整输出内容顺序的一种方法,让文件在输出文件内容中的位置与经过预处理后的输入顺序一致。

预处理输入文件

  • 编译器会对输入文件进行预处理来解析所有三斜线引用指令,在这个过程中,额外的文件会加到编译过程中。
  • 这个过程会以一些根文件开始,(命令行中指定的文件或是在 tsconfig.json中的"files"列表里的文件)按指定的顺序进行预处理。
  • 三斜线引用以它们在文件里出现的顺序,使用深度优先的方式解析(在一个文件被加入列表前,包含的所有三斜线引用都要被处理,以及它们包含的目标。)。
  • 如果不是根文件,一个三斜线引用路径是相对于包含它的文件的。

错误

  • 引用不存在的文件会报错。
  • 文件用三斜线指令引用自己会报错。

使用 --noresolve

  • 如果指定了--noResolve编译选项,三斜线引用会被忽略,不会增加新文件,也不会改变给定文件的顺序。

指令

  • /// <reference path="..." />指令声明依赖

  • /// <reference types="..." />指令声明了对某个包的依赖。对这些包的名字的解析与在 import语句里对模块名的解析类似。

  • 仅当在你需要写一个d.ts文件时才使用这个指令。

例如,把 /// <reference types="node" />引入到声明文件,表明这个文件使用了 @types/node/index.d.ts里面声明的名字。并且,这个包需要在编译阶段与声明文件一起被包含进来。

  • 对于那些在编译阶段生成的声明文件,编译器会自动地添加/// <reference types="..." />
  • 当且仅当结果文件中使用了引用的包里的声明时才会在生成的声明文件里添加/// <reference types="..." />语句。
  • 若要在.ts文件里声明一个对@types包的依赖,使用--types命令行选项或在tsconfig.json里使用 @types / typeRoots / types 指定。

可以简单地把三斜线类型引用指令当做 import声明的包。

  • /// <reference no-default-lib="true"/>指令把一个文件标记成默认库。 并告诉编译器在编译过程中不要包含这个默认库(比如,lib.d.ts)。 这与在命令行上使用 --noLib相似。

你会在 lib.d.ts文件和它不同的变体的顶端看到这个注释。

当传递了--skipDefaultLibCheck时,编译器只会忽略检查带有/// <reference no-default-lib="true"/>的文件。

  • /// <amd-module />指令允许给编译器传入一个可选的模块名

默认情况下生成的AMD模块都是匿名的。 但当一些工具需要处理生成的模块时会产生问题,比如 r.js

amdModule.ts

///<amd-module name='NamedModule'/>
export class C {
}

这会将NamedModule传入到AMD define函数里:

amdModule.js

define("NamedModule", ["require", "exports"], function (require, exports) {
    var C = (function () {
        function C() {
        }
        return C;
    })();
    exports.C = C;
});

JavaScript 文件类型检查

  • TypeScript 2.3以后的版本支持使用--checkJs.js文件进行类型检查和错误提示。
  • 通过添加// @ts-nocheck注释来忽略类型检查;
  • 通过去掉--checkJs设置并添加一个// @ts-check注释来选则检查某些.js文件。
  • 可以使用// @ts-ignore来忽略本行的错误。
  • 如果使用了tsconfig.json,JS检查将遵照一些严格检查标记,如noImplicitAnystrictNullChecks等。

但因为JS检查是相对宽松的,在使用严格标记时可能会有些出乎意料的情况。

用 JSDoc 类型表示类型信息

  • .js文件里,类型可以和在.ts文件里一样被推断出来。 当类型不能被推断时,它们可以通过 JSDoc 来指定,就好比在.ts文件中指定一般。

如同TypeScript,--noImplicitAny会在编译器无法推断类型的位置报错。

/** @type {number} */
var x;
x = 0;      // OK
x = false;  // Error: boolean is not assignable to number

JSDoc文档

属性的推断来自于类内的赋值语句

  • ES2015没提供声明类属性的方法,属性是动态赋值。因此,在.js文件里,编译器从类内部的属性赋值语句来推断属性类型。
  • 属性的类型是在构造函数里赋的值的类型,除非它没在构造函数里定义或者在构造函数里是undefinednull

若是这种情况,类型将会是所有赋的值的类型的联合类型。

  • 在构造函数里定义的属性会被认为是一直存在的,然而那些在方法,存取器里定义的属性被当成可选的。
class C {
    constructor() {
        this.constructorOnly = 0
        this.constructorUnknown = undefined
    }
    method() {
        this.constructorOnly = false // error, constructorOnly is a number
        this.constructorUnknown = "plunkbat" // ok, constructorUnknown is string | undefined
        this.methodOnly = 'ok'  // ok, but y could also be undefined
    }
    method2() {
        this.methodOnly = true  // also, ok, y's type is string | boolean | undefined
    }
}
  • 如果类的属性只是读取用的,那么就在构造函数里用JSDoc声明它的类型。
  • 如果它稍后会被初始化,则不需要在构造函数里给它赋值。
class C {
    constructor() {
        /** @type {number | undefined} */
        this.prop = undefined;
        /** @type {number | undefined} */
        this.count;
    }
}

let c = new C();
c.prop = 0;          // OK
c.count = "string";  // Error: string is not assignable to number|undefined

构造函数等同于类

  • ES2015以前,Javascript 使用构造函数代替类,typeScript编译器支持这种模式并能够将构造函数识别为ES2015的类,属性类型推断机制和上面介绍的一致。
function C() {
    this.constructorOnly = 0
    this.constructorUnknown = undefined
}
C.prototype.method = function() {
    this.constructorOnly = false // error
    this.constructorUnknown = "plunkbat" // OK, the type is string | undefined
}

支持CommonJS模块

  • .js文件里,TypeScript能识别出CommonJS模块,对exportsmodule.exports的赋值被识别为导出声明。

相似地,require函数调用被识别为模块导入。例如:

// same as `import module "fs"`
const fs = require("fs");

// same as `export function readFile`
module.exports.readFile = function(f) {
  return fs.readFileSync(f);
}

类,函数和对象字面量是命名空间

  • .js文件里的类是命名空间,可以用于嵌套类,比如:
class C {
}
C.D = class {
}
  • ES2015 之前的代码,函数可以用来模拟静态方法:
function Outer() {
  this.y = 2
}
Outer.Inner = function() {
  this.yy = 2
}
  • 类,函数,对象字面量用于创建简单的命名空间:
var ns = {}
ns.C = class {
}
ns.func = function() {
}
  • 同时还支持其它的变化:
// 立即调用的函数表达式
var ns = (function (n) {
  return n || {};
})();
ns.CONST = 1

// defaulting to global
var assign = assign || function() {
  // code goes here
}
assign.extra = 1

对象字面量是开放的

  • .ts文件里,用对象字面量初始化一个变量的同时也给它声明了类型,并且新的成员不能再被添加到对象字面量中。 而这个规则在.js文件里,对象字面量具有开放的类型,允许添加并访问原先没有定义的属性。
var obj = { a: 1 };
obj.b = 2;  // Allowed

对象字面量的表现就好比具有一个默认的索引签名[x:string]: any,它们可以被当成开放的映射而不是封闭的对象。

  • 与其它JS检查行为相似,这种行为可以通过指定JSDoc类型来改变。
/** @type {{a: number}} */
var obj = { a: 1 };
obj.b = 2;  // Error, type {a: number} does not have property b

null,undefined,和空数组的类型是any或any[]

  • 任何用nullundefined初始化的变量,参数或属性,它们的类型是any

就算是在严格null检查模式下。

  • 任何用[]初始化的变量,参数或属性,它们的类型是any[],就算是在严格null检查模式下,唯一的例外是像上面那样有多个初始化器的属性。
function Foo(i = null) {
    if (!i) i = 1;
    var j = undefined;
    j = 2;
    this.l = [];
}
var foo = new Foo();
foo.l.push(foo.i);
foo.l.push("end");

函数参数是默认可选的

  • 由于在ES2015之前无法指定可选参数,因此.js文件里所有函数参数都被当做是可选的。

使用过多的参数调用函数会得到一个错误。

function bar(a, b) {
  console.log(a + " " + b);
}

bar(1);       // OK, second argument considered optional
bar(1, 2);
bar(1, 2, 3); // Error, too many arguments
  • 使用 JSDoc 注解的函数会被从这条规则里移除,并且可使用 JSDoc 可选参数语法来表示可选性。
/**
 * @param {string} [somebody] - Somebody's name.
 */
function sayHello(somebody) {
    if (!somebody) {
        somebody = 'John Doe';
    }
    console.log('Hello ' + somebody);
}

sayHello();

arguments推断出的 var-args 参数声明

  • 如果一个函数的函数体内有对arguments的引用,那么这个函数会隐式地被认为具有一个 var-arg 参数(比如:(...arg: any[]) => any))。可使用 JSDoc 的 var-arg 语法来指定arguments的类型。
/** @param {...number} args */
function sum(/* numbers */) {
    var total = 0
    for (var i = 0; i < arguments.length; i++) {
      total += arguments[i]
    }
    return total
}

未指定的类型参数默认为any

  • 由于 JavaScript 里没有一种自然的语法来指定泛型参数,因此未指定的参数类型默认为any

  • 在 extends 语句中如 React.Component 被定义成具有两个类型参数,PropsState。 而在一个.js文件里,没有一个合法的方式在 extends 语句里指定它们,默认地参数类型为any

import { Component } from "react";

class MyComponent extends Component {
    render() {
        this.props.b; // Allowed, since this.props is of type any
    }
}

使用JSDoc的@augments来明确地指定类型。

import { Component } from "react";

/**
 * @augments {Component<{a: number}, State>}
 */
class MyComponent extends Component {
    render() {
        this.props.b; // Error: b does not exist on {a:number}
    }
}
  • JSDoc 里未指定的类型参数默认为any
/** @type{Array} */
var x = [];

x.push(1);        // OK
x.push("string"); // OK, x is of type Array<any>

/** @type{Array.<number>} */
var y = [];

y.push(1);        // OK
y.push("string"); // Error, string is not assignable to number
  • 泛型函数的调用使用arguments来推断泛型参数。

有时候,这个流程不能够推断出类型(大多是因为缺少推断的源),在这种情况下,类型参数类型默认为any

var p = new Promise((resolve, reject) => { reject() });

p; // Promise<any>;

支持的JSDoc

  • 列表列出了当前所支持的JSDoc注解,你可以用它们在JavaScript文件里添加类型信息。它们代表的意义与usejsdoc.org上面给出的通常是一致的或者是它的超集。
    • @type
    • @param (or @arg or @argument)
    • @returns (or @return)
    • @typedef
    • @callback
    • @template
    • @class (or @constructor)
    • @this
    • @extends (or @augments)
    • @enum

没有在下面列出的标记(例如@async)都是还不支持的。

  • @type标记并引用一个类型名称(原始类型,TypeScript 里声明的类型,或在JSDoc 里@typedef标记指定的) 可以使用任何 TypeScript 类型和大多数 JSDoc 类型。
/**
 * @type {string}
 */
var s;

/** @type {Window} */
var win;

/** @type {PromiseLike<string>} */
var promisedString;

// You can specify an HTML Element with DOM properties
/** @type {HTMLElement} */
var myElement = document.querySelector(selector);
element.dataset.myData = '';

@type可以指定联合类型—例如,stringboolean类型的联合。

/**
 * @type {(string | boolean)}
 */
var sb;

// 括号可选
/**
 * @type {string | boolean}
 */
var sb;

有多种方式来指定数组类型:

/** @type {number[]} */
var ns;
/** @type {Array.<number>} */
var nds;
/** @type {Array<number>} */
var nas;

指定对象字面量类型。 例如,一个带有a(字符串)和b(数字)属性的对象,使用下面的语法:

/** @type {{ a: string, b: number }} */
var var9;

使用字符串和数字索引签名来指定map-likearray-like的对象,使用标准的 JSDoc 语法或者 TypeScript 语法。

// 类字典
/**
 *
 * @type {Object.<string, number>}
 */
var stringToNumber;

// 类数组
/** @type {Object.<number, object>} */
var arrayLike;

使用TypeScript或Closure语法指定函数类型。

/** @type {function(string, boolean): number} Closure syntax */
var sbn;
/** @type {(s: string, b: boolean) => number} Typescript syntax */
var sbn2;

直接使用未指定的Function类型:

/** @type {Function} */
var fn7;
/** @type {function} */
var fn6;

Closure的其它类型也可以使用:

/**
 * @type {*} - can be 'any' type
 */
var star;
/**
 * @type {?} - unknown type (same as 'any')
 */
var question;

转换TypeScript 借鉴了 Closure 里的转换语法。 在括号表达式前面使用@type标记,可以将一种类型转换成另一种类型

/**
 * @type {number | string}
 */
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString)

使用导入类型从其它文件中导入声明。 这个语法是TypeScript特有的,与JSDoc标准不同:

/**
 * @param p { import("./a").Pet }
 */
function walk(p) {
    console.log(`Walking ${p.name}...`);
}

导入类型也可以使用在类型别名声明中

/**
 * @typedef Pet { import("./a").Pet }
 */

/**
 * @type {Pet}
 */
var myPet;
myPet.name;

导入类型可以用在从模块中得到一个值的类型。

/**
 * @type {typeof import("./a").x }
 */
var x = require("./a").x;
  • @param语法和@type相同,但增加了一个参数名。 使用[]可以把参数声明为可选的:
// Parameters may be declared in a variety of syntactic forms
/**
 * @param {string}  p1 - A string param.
 * @param {string=} p2 - An optional param (Closure syntax)
 * @param {string} [p3] - Another optional param (JSDoc syntax).
 * @param {string} [p4="test"] - An optional param with a default value
 * @return {string} This is the result
 */
function stringsStringStrings(p1, p2, p3, p4){
  // TODO
}

函数的返回值类型也是类似的

/**
 * @return {PromiseLike<string>}
 */
function ps(){}

/**
 * @returns {{ a: string, b: number }} - May use '@returns' as well as '@return'
 */
function ab(){}
  • @typedef可以用来声明复杂类型和@param类似的语法。
/**
 * @typedef {Object} SpecialType - creates a new type named 'SpecialType'
 * @property {string} prop1 - a string property of SpecialType
 * @property {number} prop2 - a number property of SpecialType
 * @property {number=} prop3 - an optional number property of SpecialType
 * @prop {number} [prop4] - an optional number property of SpecialType
 * @prop {number} [prop5=42] - an optional number property of SpecialType with default
 */
/** @type {SpecialType} */
var specialTypeObject;

可以在第一行上使用objectObject

/**
 * @typedef {object} SpecialType1 - creates a new type named 'SpecialType'
 * @property {string} prop1 - a string property of SpecialType
 * @property {number} prop2 - a number property of SpecialType
 * @property {number=} prop3 - an optional number property of SpecialType
 */
/** @type {SpecialType1} */
var specialTypeObject1;
  • @param允许使用相似的语法。

嵌套的属性名必须使用参数名做为前缀:

/**
 * @param {Object} options - The shape is the same as SpecialType above
 * @param {string} options.prop1
 * @param {number} options.prop2
 * @param {number=} options.prop3
 * @param {number} [options.prop4]
 * @param {number} [options.prop5=42]
 */
function special(options) {
  return (options.prop4 || 1001) + options.prop5;
}
  • @callback@typedef相似,但它指定函数类型而不是对象类型:
/**
 * @callback Predicate
 * @param {string} data
 * @param {number} [index]
 * @returns {boolean}
 */
/** @type {Predicate} */
const ok = s => !(s.length % 2);
  • 所有这些类型都可以使用TypeScript的语法@typedef在一行上声明:
/** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */
/** @typedef {(data: string, index?: number) => boolean} Predicate */
  • 使用@template声明泛型:
/**
 * @template T
 * @param {T} p1 - A generic parameter that flows through to the return type
 * @return {T}
 */
function id(x){ return x }

用逗号或多个标记来声明多个类型参数:

/**
 * @template T,U,V
 * @template W,X
 */

还可以在参数名前指定类型约束,只有列表的第一项类型参数会被约束:

/**
 * @template {string} K - K must be a string or string literal
 * @template {{ serious(): string }} Seriousalizable - must have a serious method
 * @param {K} key
 * @param {Seriousalizable} object
 */
function seriousalize(key, object) {
  // ????
}
  • @constructor:让检查更严格提示更友好。(虽然编译器通过this属性的赋值来推断构造函数)
/**
 * @constructor
 * @param {number} data
 */
function C(data) {
  this.size = 0;
  this.initialize(data); // Should error, initializer expects a string
}
/**
 * @param {string} s
 */
C.prototype.initialize = function (s) {
  this.size = s.length
}

var c = new C(0);
var result = C(1); // C should only be called with new

通过@constructorthis将在构造函数C里被检查。

因此你在initialize方法里传入一个数字,将得到一个错误提示。

如果你直接调用C而不是构造它,也会得到一个错误。这意味着那些既能构造也能直接调用的构造函数不能使用@constructor

  • @this明确指定它的类(虽然编译器通常可以通过上下文来推断出this的类型)。
/**
 * @this {HTMLElement}
 * @param {*} e
 */
function callbackForLater(e) {
    this.clientHeight = parseInt(e) // should be fine!
}
  • @extends :当 JavaScript 类继承了一个基类,指定类型参数的类型。
/**
 * @template T
 * @extends {Set<T>}
 */
class SortableSet extends Set {
  // ...
}

@extends只作用于类。当前,无法实现构造函数继承类的情况。

  • @enum标记允许你创建一个对象字面量,它的成员都有确定的类型。

不同于JavaScript里大多数的对象字面量,它不允许添加额外成员。

/** @enum {number} */
const JSDocState = {
  BeginningOfLine: 0,
  SawAsterisk: 1,
  SavingComments: 2,
}

@enum与TypeScript的@enum大不相同,它更加简单且可以是任何类型:

/** @enum {function(number): number} */
const Math = {
  add1: n => n + 1,
  id: n => -n,
  sub1: n => n - 1,
}

其他示例

var someObj = {
  /**
   * @param {string} param1 - Docs on property assignments work
   */
  x: function(param1){}
};

/**
 * As do docs on variable assignments
 * @return {Window}
 */
let someFunc = function(){};

/**
 * And class methods
 * @param {string} greeting The greeting to use
 */
Foo.prototype.sayHi = (greeting) => console.log("Hi!");

/**
 * And arrow functions expressions
 * @param {number} x - A multiplier
 */
let myArrow = x => x * x;

/**
 * Which means it works for stateless function components in JSX too
 * @param {{a: string, b: number}} test - Some param
 */
var sfc = (test) => <div>{test.a.charAt(0)}</div>;

/**
 * A parameter can be a class constructor, using Closure syntax.
 *
 * @param {{new(...args: any[]): object}} C - The class to register
 */
function registerClass(C) {}

/**
 * @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
 */
function fn9(p1) {
  return p1.join();
}

不支持模式

  • 在值空间中将对象视为类型是不可以的,除非对象创建了类型,如构造函数。
function aNormalFunction() {

}
/**
 * @type {aNormalFunction}
 */
var wrong;
/**
 * Use 'typeof' instead:
 * @type {typeof aNormalFunction}
 */
var right;
  • 对象字面量属性上的=后缀不能指定这个属性是可选的:
/**
 * @type {{ a: string, b: number= }}
 */
var wrong;
/**
 * Use postfix question on the property name instead:
 * @type {{ a: string, b?: number }}
 */
var right;
  • Nullable类型只在启用了strictNullChecks检查时才启作用:
/**
 * @type {?number}
 * With strictNullChecks: true -- number | null
 * With strictNullChecks: off  -- number
 */
var nullable;
  • Non-nullable类型没有意义,以其原类型对待:
/**
 * @type {!number}
 * Just has type number
 */
var normal;

不同于 JSDoc 类型系统,TypeScript 只允许将类型标记为包不包含null,没有明确的Non-nullable

如果启用了strictNullChecks,那么number是非null的。

如果没有启用,那么number是可以为null的。

关于其他

ts-node、nodemon自动化

  • ts-node

    • npm install -D ts-node

      • 安装 ts-node
    • ts-node ts文件名 -> 运行该文件 -> 自动编译和运行ts

      • 把编译后的js文件放在内存中
      • 若要编译后的js文件 -> 还是需要tsc 文件名
  • nodemon:监控并自动化更新

    • npm install nodemon -S -D
      • 安装 nodemon 插件
    • nodemon —exec 文件名 文件目录
      • nodemon —exec ts-node ./index.ts

TypeScript 封装统一操作数据库

// 定义一个操作数据库的库 -> 可以支持Mysql、Mssql、Mongodb
// 需要约束统一规范、以及代码重用 -> 接口(统一规范)、重用(泛型)

// 定义数据库操作接口
interface DBI<T>{   //泛型接口
	add(info:T):boolean;
	update(info:T, id:number):boolean;
	delete(id:number):boolean;
	get(id:number):any[];
}

// 要接泛型接口 -> 该类也应该是泛型类
// 定义一个操作mysql数据库的类
class MysqlDb<T> implements DBI<T>{
	add(info:T):boolean{
		return true;
	}
	update(info:T, id:number):boolean{
		return true;
	}
	delete(id:number):boolean{
		return true;
	}
	get(id:number):any[]{
		return [true];
	}
}

// 同理定义MsSqlDb和MongoDb的类


// 增加Mysql数据库
// 定义一个User类和数据库表做映射
class User{
	userName:string | undefined;
	password:string | undefined;
}

let u = new User();
u.userName = '张三';
u.password = '123456';

let mysql = new MysqlDb<User>();		// 类作为参数来约束数据传入的类型
mysql.add(u);		// 传入u实例数据

// 切换对应数据库就切换类

初始化 TS 配置文件

tsconfig.json

  1. 在终端初始化:tsc - -init
  2. 在根目录生成了 tsconfig.json配置文件
  3. 使用该配置文件,在终端 tsc。

当命令行上指定了输入文件时,tsconfig.json文件会被忽略。

lodash - 使用第三方插件

引入 lodash 现代化的工具库:操作数组、函数等

  • npm install lodash -S
  • 进入文件中导入 import * as _ from 'lodash';
    • 若是第三方插件不是用ts编写
      • 编译器不会提示和补全代码
    • 去找相关库的ts文档(是声明相关库的ts源码)
      • 可能还需要引入源码库