TypeScript 知识

193 阅读8分钟

TypeScript

TypeScript 重点知识整理。

Null 和 undefined 类型


常规类型检查模式下:

类型检查器认为null和undefined赋值给一切。实际上,null和undefined是每一个类型的有效值, 并且不能明确排除它们(因此不可能检测到错误)

--strictNullChecks严格空检查模式中:

可以切换到新的严格空检查模式中。 null和undefined值不再属于任何类型的值,仅仅属于它们自己类型和any类型的值 (还有一个例外, undefined也能赋值给void)。

TypeScript2.0 操作符


严格空检查模式中前提下:

可选操作符:

x?: number; // x的类型是 number | undefined

表达式操作符:

a: number | null; // a的类型是 number | null

非空断言操作符:

let s = e!.name; // 断言e是非空并访问name属性,强行告诉编译器,不要检查这里了,我断言了!

运算x!产生一个不包含null和undefined的x的值。断言的形式类似于x和x as T

never类型:

never是永不返回函数的“返回类型”,也是变量在类型保护中永不为true的类型。

// 函数返回never必须无法执行到终点,例子:

function error(message: string): never {
    throw new Error(message);
}

只读属性和索引签名:

属性或索引签名现在可以使用readonly修饰符声明为只读的。

interface Point { readonly x: number; readonly y: number; }

let b: ReadonlyArray<number> = [1, 2, 3];

TypeScript2.0 接口


interface SquareConfig {
  color?: string;
  width?: number;
  readonly x: number; // 只读属性
  // [x: number]: string; 
  // [x: string]: string;
  // 数字索引的返回值必须是字符串索引返回值类型的子类型
}
// 函数类型接口,定义了SearchFunc方法参数和返回值的方法类型
interface SearchFunc {
  (source: string, subString: string): boolean;
}

TypeScript2.0 类


class Test{
	static A: string;
	B: string;
	test() {
		return Test.A + '-' + this.B;
	}
}

等于:

let Test = (function(){
	function Test() {
		this.B:string;
	}
	Test.A:string;
	Test.prototype.test = function() {
		return Test.A + '-' + This.B;
	}
	return Test;
})();

类的属性实际绑定在方法上。

类的方法和静态属性绑在 prototype 上。

let C: Test = new Test(); // 声明了 Test 实例类型
let D: typeof Test = Test; // 声明了 Test 方法类型

TypeScript2.0 函数


let myAdd: (baseValue:number, increment:number) => number =
    function(x: number, y: number): number { return x + y; };

只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。

interface UIElement {
	addClickListener(onclick: (this: void, e: Event) => void): void;
}
// 如果存在某一个库接口,传入的参数方法中有this: void表示不需要this的类型声明的话。以下做法:
class Handler {
    info: string;
    onClickBad(this: Handler, e: Event) {
        this.info = e.message;
    };
    onClickBadVoid(this: void, e: Event) {
        this.info = e.message;
    };
    onClickBadF = (e: Event) => {
        this.info = e.message;
    };
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // ① 这是报错的,因为你隐式声明了,this的类型, 这是会带入this进入方法里。
uiElement.addClickListener(h.onClickBadVoid); // ② 这是报错的,这样参数类型正确了,但是方法体内不能使用this了。this为undefined
uiElement.addClickListener(h.onClickBadF); // ③这是没问题的,因为参数类型正确,并且箭头函数会在this字面量等于上下文的Handler,可以使用this

③:缺点是每个Handler对象都会创建一个箭头函数,因为这时onClickBadF为一个类的属性,而不是方法,只不过属性指向了方法,不绑在原型链上。

①、②:方法只会被创建一次,添加到 Handler的原型链上。 它们在不同 Handler对象间是共享的。

TypeScript2.0 泛型


// 泛型接口
interface GenericIdentityFnA {
    <T>(arg: T): T;
}
interface GenericIdentityFnB<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}
// 这里让 myIdentity 指向 identity ,并不是执行 identity 函数
let myIdentity: GenericIdentityFnA = identity;
let myIdentity: GenericIdentityFnB<number> = identity;
// 泛型类
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

TypeScript2.0 类型推论


let x = [0, 1, null]; // 候选类型为:number | null

计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。最终的通用类型取自候选类型。

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

这情况下候选类型为:Rhino | Elephant | Snake,以此TS计算不出来他们的通用类型,并且在这种情况下会给出{}这种类型给zoo,但是之后使用zoo访问成员的时候,就会报错了,因为{}没有任何成员。

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

这情况下通用类型为:Animal[] , 候选类型为:Rhino | Elephant | Snake。

TypeScript2.0 类型兼容性


TS 的类型兼容性是建立在结构类型上的,不像 C# 或 Java 的是建立在名义类型上的,例如:

interface Named {
    name: string;
}

class Person {
    name: string;
}

let p: Named;
// OK, 因为是建立在结构类型上的,结构符合
p = new Person();

可以看出,上面代码段是没问题的,但是如果在 Java 上是肯定报错的,因为:因为Person类没有明确说明其实现了Named接口。

—兼容规则—

interface Named {
    name: string;
}

let x: Named;
let y = { name: 'Alice', location: 'Seattle' };
x = y;
y = x; // Error,y 对 x 说使用我的人可能要访问 location 你没有
// 解释:Named有name,y 也有name ,那就满足了①。
// 但是在这是后使用x访问成员变量时只能访问name,访问不到location.
// 就像类实现接口后,将类实例赋予接口类型的变量。

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

y = x; // OK
x = y; // Error
// 解释: 函数参数满足的是③,因为,y函数体内表明可能会用到s,
// 如果将x赋予y,y就说:x你不用s那就随你便吧,就像 Array#forEach
// 如果将y赋予x,x就说:y你要用s?我这里没有你s的位置,请滚吧!

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

x = y; // OK
y = x; // Error because x() lacks a location property
// 解释: 函数返回类型满足③
// 因为返回的类型可以和①一样理解即可,
// 返回的类型如果只有一个属性,返回多了,我也不管,也访问不到了

① 类型兼容对于对象来说为: 兼容于

② 类型兼容对于函数返回值来说为: 兼容于

③ 类型兼容对于函数参数来说为: 兼容于

枚举类型之间是不兼容的。

TypeScript2.0 声明模块 declare


使用其他外部模块的时候要想描述非TypeScript编写的类库的类型,我们需要声明类库所暴露出的API。

我们叫它声明因为它不是“外部程序”的具体实现。 它们通常是在 .d.ts文件里定义的。

// node.d.ts (simplified excerpt)
declare module "url" {
    export interface Url {
        protocol?: string;
        hostname?: string;
        pathname?: string;
    }

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

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

使用的时候为:

import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");

假如你不想在使用一个新模块之前花时间去编写声明,你可以采用声明的简写形式以便能够快速使用它。

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

使用的时候:

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

全局扩展:

// observable.ts
export class Observable<T> {
    // ... still no implementation ...
}

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

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

模块声明通配符

某些模块加载器如SystemJS 和 AMD支持导入非JavaScript内容。 它们通常会使用一个前缀或后缀来表示特殊的加载语法。 模块声明通配符可以用来表示这些情况。

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

使用的时候:

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

文件保存的时候还是使用的是 .txt/.json 的格式、但是使用的时候就可以根据声明的样子来进行import 例如:./xyz.txt!text

TypeScript2.0 命名空间


外部命名空间 流行的程序库D3在全局对象d3里定义它的功能。 因为这个库通过一个 <script>标签加载(不是通过模块加载器),它的声明文件使用内部模块来定义它的类型。 为了让TypeScript编译器识别它的类型,我们使用外部命名空间声明。 比如,我们可以像下面这样写:

// 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 let d3: D3.Base;

TypeScript2.0 模块解析


模块导入分为相对和非相对导入方式

相对导入是以/,./或../开头的。 下面是一些例子:

  • import Entry from "./components/Entry";
  • import { DefaultHeaders } from "../constants/http";
  • import "/mod";

所有其它形式的导入被当作非相对的。 下面是一些例子:

  • import * as $ from "jQuery";
  • import { Component } from "angular2/core";

TypeScript 模块导入策略(node导入策略类似的,因为ts也是建立在nods之上)

比如,有一个导入语句import { b } from "./moduleB"在/root/src/moduleA.ts里,会以下面的流程来定位"./moduleB":

  1. /root/src/moduleB.ts
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json (如果指定了"typings"属性)
  5. /root/src/moduleB/index.ts
  6. /root/src/moduleB/index.tsx
  7. /root/src/moduleB/index.d.ts

如果是非相对导入的话:

  1. /root/src/node_modules/moduleB.ts
  2. /root/src/node_modules/moduleB.tsx
  3. /root/src/node_modules/moduleB.d.ts
  4. /root/src/node_modules/moduleB/package.json (如果指定了"typings"属性)
  5. /root/src/node_modules/moduleB/index.ts
  6. /root/src/node_modules/moduleB/index.tsx
  7. /root/src/node_modules/moduleB/index.d.ts

往上一层走:

  1. /root/node_modules/moduleB.ts
  2. /root/node_modules/moduleB.tsx
  3. /root/node_modules/moduleB.d.ts
  4. /root/node_modules/moduleB/package.json (如果指定了"typings"属性)
  5. /root/node_modules/moduleB/index.ts
  6. /root/node_modules/moduleB/index.tsx
  7. /root/node_modules/moduleB/index.d.ts

之后继续往上一层走...

TypeScript2.0 声明文件说明


global.d.ts 用于声明全局库。
module-class.d.ts 用于声明可构造的模块 var x = require("bar"); new x()
module-function.d.ts 用于声明函数模块 var x = require("bar"); x()
module.d.ts 用于声明不能被调用和构造的模块
global-plugin.d.ts 用于声明全局插件
module-plugin.d.ts 用于声明模块插件
global-modifying-module.d.ts 用于声明能够修改全局作用域的模块
某库依赖全局库:

/// <reference types="someLib" /> // 使用三斜杠指令的 reference
function getThing(): someLib.thing;

依赖于某模块:

import * as moment from "moment";
function getThing(): moment;

依赖于UMD库:

如果你是全局库需要依赖UMD库,那就用:

/// <reference types="moment" />
function getThing(): moment;

如果你是模块库需要依赖于UMD库,那就用:

import * as someLib from 'someLib';