TypeScript 学习笔记

简介

在 TypeScript 的学习过程中,我发现这个语言有点抽象,团队成员那边已经给出了基础部分的学习笔记,我这边就记录一下学习过程中遇到的难点。

接口的妙用

混合类型

接口能够描述 JavaScript 里丰富的类型。 因为 JavaScript 其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。

一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
type ApiResponse<T> =
  | {status: 'success'; data: T; timestamp: Date }
  | {status: 'error'; message: string; timestamp: Date };
let response1: ApiResponse<number> = {
  status: 'success',
  data: 100,
  timestamp: new Date()
};
let response2: ApiResponse<number> = {
  status: 'error',
  message: 'There was an error',
  timestamp: new Date()
};

类的一些特性

存取器

首先,我们从一个没有使用存取器的例子开始。

class Employee {
    fullName: string;
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

我们可以随意的设置 fullName ,这是非常方便的,但是这也可能会带来麻烦。

下面这个版本里,我们先检查用户密码是否正确,然后再允许其修改员工信息。 我们把对 fullName 的直接访问改成了可以检查密码的 set 方法。 我们也加了一个 get 方法,让上面的例子仍然可以工作。

let passcode = "secret passcode";

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    alert(employee.fullName);
}

我们可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改员工。

  • 只带有get不带有set的存取器自动被推断为readonly。 这在从代码生成.d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。

  • ( TypeScript 简直是语法大杂烩, get 和 set 关键字都来了

函数

剩余参数

必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用arguments来访问所有传入的参数。

在TypeScript里,你可以把所有参数收集到一个变量里:

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

this

  • 这个好复杂,可以看文档

枚举

怪异的枚举值

enum FileAccess {
    // constant members
    None,
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
    // computed member
    G = "123".length
}

断言

  1. 尖括号语法: 尖括号语法是 TypeScript 中最早引入的断言形式。它使用 <Type> ,其中 Type 是您要将值断言为的目标类型。

    let someValue: any = "Hello, World!";
    let strLength: number = (<string>someValue).length;
  2. as 关键字: "as" 关键字是更现代和推荐的断言语法,它在 JSX 和 TSX 中也更有用。它使用value as Type形式。

    let someValue: any = "Hello, World!";
    let strLength: number = (someValue as string).length;

高级类型

交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如,Person & Serializable & Loggable 同时是 Person Serializable Loggable 。 就是说这个类型的对象同时拥有了这三种类型的成员

类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}

同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:

type Container<T> = { value: T };

与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

#索引类型(Index types)(十分抽象的家伙

使用索引类型,编译器就能够检查使用了动态属性名的代码。 例如,一个常见的JavaScript模式是从对象中选取属性的子集。

function pluck(o, names) {
    return names.map(n => o[n]);
}

下面是如何在 TypeScript 里使用此函数,通过索引类型查询索引访问操作符:

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}

interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Jarid',
    age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]

编译器会检查 name 是否真的是 Person 的一个属性。 本例还引入了几个新的类型操作符。 首先是 keyof T ,索引类型查询操作符。 对于任何类型 T,keyof T 的结果为 T 上已知的公共属性名的联合。 例如:

let personProps: keyof Person; // 'name' | 'age'

keyof Person 是完全可以与'name' | 'age'互相替换的。 不同的是如果你添加了其它的属性到 Person ,例如 address : string,那么 keyof Person 会自动变为 'name' | 'age' | 'address' 。 你可以在像 pluck 函数这类上下文里使用 keyof ,因为在使用之前你并不清楚可能出现的属性名。 但编译器会检查你是否传入了正确的属性名给 pluck :

pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'

第二个操作符是 T[K] ,索引访问操作符。 在这里,类型语法反映了表达式语法。 这意味着 person['name'] 具有类型Person['name'] — 在我们的例子里则为string类型。 然而,就像索引类型查询一样,你可以在普通的上下文里使用T[K],这正是它的强大所在。 你只要确保类型变量 K extends keyof T 就可以了。 例如下面 getProperty 函数的例子:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name]; // o[name] is of type T[K]
}

getProperty 里的 o: T和name: K ,意味着 o[name]: T[K] 。 当你返回 T[K] 的结果,编译器会实例化键的真实类型,因此 getProperty 的返回值类型会随着你需要的属性改变。

let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'

#映射类型

一个常见的任务是将一个已知的类型每个属性都变为可选的:

interface PersonPartial {
    name?: string;
    age?: number;
}

或者我们想要一个只读版本:

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}

这在 JavaScript 里经常出现,TypeScript 提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为 readonly 类型或可选的。 下面是一些例子:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}

像下面这样使用:

type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

#由映射类型进行推断

现在你了解了如何包装一个类型的属性,那么接下来就是如何拆包。 其实这也非常容易:

function unproxify<T>(t: Proxify<T>): T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}

let originalProps = unproxify(proxyProps);

注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。

命名空间和模块

使用命名空间

命名空间是位于全局命名空间下的一个普通的带有名字的 JavaScript 对象。 这令命名空间十分容易使用。 它们可以在多文件中同时使用,并通过 --outFile 结合在一起。 命名空间是帮你组织 Web 应用不错的方式,你可以把所有依赖都放在 HTML 页面的 <script> 标签里。

但就像其它的全局命名空间污染一样,它很难去识别组件之间的依赖关系,尤其是在大型的应用中。

使用模块

像命名空间一样,模块可以包含代码和声明。 不同的是模块可以声明它的依赖。

模块会把依赖添加到模块加载器上(例如 CommonJs / Require.js )。 对于小型的 JS 应用来说可能没必要,但是对于大型应用,这一点点的花费会带来长久的模块化和可维护性上的便利。 模块也提供了更好的代码重用,更强的封闭性以及更好的使用工具进行优化。

对于 Node.js 应用来说,模块是默认并推荐的组织代码的方式。

从 ECMAScript 2015 开始,模块成为了语言内置的部分,应该会被所有正常的解释引擎所支持。 因此,对于新项目来说推荐使用模块做为组织代码的方式。

Life is a Rainmeter