TypeScript基础用法

380 阅读26分钟

引言

先附一个不错的TS学习网站

本文侧重:

  • 学完官方文档后的速查(标题分得细,方便定位😄);
  • 会用//注释下与JS的某些不同的地方(习惯了JS,脑子偶尔转不过弯)。

例子还是用的官网的例子。下面开始正文吧!

基础类型 首字母都是小写

布尔

let b : boolen = true

数字

let decLiteral: number = 6;

字符串

let name: string = "bob";

数组

方式一:

let list: number[] = [1, 2, 3];

方式二:

let list : Array<number> = [1, 2, 3];

元组 Tuple

表示一个已知元素数量和类型的数组,各元素的类型不必相同,数组是有序的:

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

当访问一个越界的元素,会使用联合类型替代:

x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型

console.log(x[5].toString()); // OK, 'string''number' 都有 toString

x[6] = true; // Error, 布尔不是(string | number)类型

枚举 enum类型

默认从0开始编号,也可以用=自定义起始编号。

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green; // 2 这里的c可以标记为当前枚举的类型,也可以标记为数字

全部手动赋值:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

又枚举的值得到它的名字:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2]; //名字不能用枚举的类型,只能string

console.log(colorName);  // 显示'Green'因为上面代码里它的值是2

any 类型

对于编译阶段不清楚的类型指定一个类型,让其通过编译,可以用any。
给变量定义这个类型,可以跳过类型检查。

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

在对于包含不同类型的数组时,十分有用:

let list: any[] = [1, true, "free"];

list[1] = 100;

void 类型

对于函数,表示没有返回值:

function warnUser(): void {
    console.log("This is my warning message");
}

定义一个void的变量,只能赋值undefined或null。

null和undefined 类型

只能这么赋值:

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

是所有类型的子类型,比如能赋值给number类型等。不过,当你指定了--strictNullChecks标记,null和undefined只能赋值给void和它们各自

never 类型

never类型表示的是那些永不存在的值的类型。如:抛出异常、死循环等等。

never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never。

object 类型

object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

对象类型

function printLabel(labelledObj: { label: string }) {
  console.log(labelledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj); // ok

类型断言

两种语法,ts的jsx只支持as语法。
方式一,用< >:

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

方式二,用as:

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

变量声明

let const 解构 展开运算符 和js没什么区别

接口

TypeScript的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。

初探接口

作为函数参数时,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配,如下:

function printLabel(labelledObj: { label: string }) {
  console.log(labelledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

用接口重写上面的例子:

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

LabelledValue接口就好比一个名字,用来描述上面例子里的要求。
注意,接口不会检查属性顺序。

额外的属性检查

对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。上面例子如果调用函数时传入一个对象字面量,如下

printLabel({size: 10, label: "Size 10 Object"});// 出错,多了额外属性size
const test : LabelledValue = { size: 10, label: 'Size 10 Object' }; // 出错,多了额外属性

绕过检查有:

  • 方法一:类型断言:
printLabel({size: 10, label: "Size 10 Object"} as LabelledValue);
  • 方法二:添加一个字符串索引签名:
interface LabelledValue {
  label : string;
  [prop : string] : any  // 表示任意数量任意类型的属性
}
  • 方法三:用变量代替字面量:
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

可选属性

interface SquareConfig {
  color?: string;
  width?: number;
}

好处:

  • 对可能存在的属性进行预定义;
  • 可以检查拼写错误(相似的时候会提示。

只读属性

创建完后不能更改。

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

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

ts具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:

a = ro as number[];

函数类型

除了上面的普通对象类型,还能定义函数类型:

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
}

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。

函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。

如果你不想指定类型,TypeScript的类型系统会推断出参数类型,也能推断出返回类型:

let mySearch: SearchFunc;
mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}

注意函数类型不要和某个属性为函数的类型搞混,比如:

interface Example{
	getName():string;  // 也可以写成getName:() => string
 }

可索引类型

以描述那些能够“通过索引得到”的类型,比如a[10]ageMap["daniel"]。 可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。如下,描述了一个数字索引并返回字符串值:

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。

字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.propertyobj["property"]两种形式都可以。 下面的例子里, name的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:

interface NumberDictionary {
  [index: string]: number;
  length: number;    // 可以,length是number类型
  name: string       // 错误,`name`的类型与索引类型返回值的类型不匹配
}

最后,可以将索引签名设置为只读,防止给索引赋值:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

类类型

接口描述了类的公共部分,它不会帮你检查类是否具有某些私有成员。

interface ClockInterface { // 当然这个接口也可以用来描述普通对象
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内:

interface ClockConstructor {
    new (hour: number, minute: number);
}

class Clock implements ClockConstructor { // 不能这样用
    currentTime: Date;
    constructor(h: number, m: number) { }
}

可以这样用:

// 表示这个类型是构造函数类型
interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
    tick();
}
 
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("tick tock");
    }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

继承接口

一个借口可以继承一个或多个接口:

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型

比如,一个对象可以同时做为函数和对象使用,并带有额外的属性:

interface Counter {
    (start: number): string; // 首先它是一个函数
    interval: number; // 有interval属性
    reset(): void; //有reset方法
}

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

接口继承类

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。接口同样会继承类的private和protected成员。所以当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

class Control {
    private state: any;
}

interface SelectableControl extends Control {
    select(): void;
}

class Button extends Control implements SelectableControl {
    select() { }
}

// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl {
    select() { }
}

基础用法

虽然和JS长得差不多,但这里毕竟是TS,还是不一样的。

class Greeter {
    greeting: string; // 属性需要额外声明下类型,js肯定是没有的。
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

继承extends

没什么特别的,不写了。和js一样,派生类包含构造函数时,别忘记super()。

共有、私有保护修饰符

放在属性前面。举个例子:

class Animal {
    private name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

默认 public

没什么特别的效果。

private

只能在声明它的类内部访问,不能外部访问。

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // 错误: 'name' 是私有的.

protected

和private很像,不同的是也能在派生类内部访问。

readonly

设置属性为只读。必须在声明时或者构造函数中初始化。

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.

参数属性

有修饰符的时候可简写,把声明和赋值合并到一处。上面readonly的例子可以简写为:

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}

存取器

setter和getter,和js用法一样。另外,不带set的存取器被自动推断为readonly。例子:

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);
}

静态属性 static

和js一样。静态属性是定义在类上的,而不是实例上的。定义在类上的,就是定义在构造函数这个对象上的。对象怎么访问属性,省略~

抽象类 abstract

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 abstract关键字用于:

  • 定义抽象类
  • 抽象类内部定义抽象方法。
abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含 abstract关键字并且可以包含访问修饰符。

abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log('Department name: ' + this.name);
    }

    abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {

    constructor() {
        super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
    }

    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }

    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}

let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

高级技巧

typeof

可以用typeof获取类的类型,而不是实例类型。

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());

把类当接口使用

类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。

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

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

函数

书写函数

返回类型一般省略,因为TS能自动推断出。

function add(x: number, y: number): number {
    return x + y;
}

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

完整的函数/函数类型

函数类型:(x: number, y: number) => number参数和返回值类型,都不能省略。

推断类型:省略一边可推断另一边,这叫“按上下文归类”。

let myAdd: (x: number, y: number) => number =     //x,y不需要和函数参数名一样
    function(x: number, y: number): number { return x + y; };

可选参数和默认参数

TS中传递给一个函数的参数,必须与期望的个数一致。

可选参数

可选参数必须跟在必须参数后面。用 '?'表示:

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

let result1 = buildName("Bob");  // works correctly now
let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result3 = buildName("Bob", "Adams");  // ah, just right

默认参数

用法与js没什么区别,根据位置不传或者传undefined获取默认值。

function buildName(firstName: string, lastName = "Smith") {
    // ...
}

不定参数

和js一样,不知道要传多少个值时候使用。

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

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

this

期望this的值类型,用假参数,放在参数列表的最前面:

function f(this: void) {
    // make sure `this` is unusable in this standalone function
}

函数重载

js没这玩意。因为ts区分了类型,故可以搞重载。
注意:function pickCard(x): any 不是重载的一部分,这里只有两个重载。

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
 }

泛型

泛型函数

定义一个带泛型的函数:

function identity<T>(arg: T): T {
    return arg;
}

两种调用方式:

  • 方式一:
let output = identity<string>("hello");
  • 方式二,更常用,利用了类型推论:
let output = identity("hello");

使用泛型变量

像上面例子使用泛型时,你必须把这些参数当做是任意或所有类型。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

想要使用length属性,可以用泛型定义数组:

function loggingIdentity<T>(arg: T[]): T[] {  // T[] 也可以换成 Array<T>
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

函数类型

let myIdentity: <T>(arg: T) => T = identity;

也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以:

let myIdentity: <U>(arg: U) => U = identity;

也可以用带有调用签名的对象字面量定义:

let myIdentity: {<T>(arg: T): T} = identity;

函数类型的接口:

interface GenericIdentityFn {
    <T>(arg: T): T;   // 当我对当前函数传什么类返回什么类型时
}

把泛型参数当作整个接口的一个参数:

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity; // 当我想匹配不同的函数时

考虑何时把参数放在调用签名里和何时放在接口上。

泛型类

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

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

泛型约束

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error, number doesn't have a .length property

在泛型约束中使用类型参数

声明一个参数,被另一个参数所约束:

function getProperty(obj: T, key: K) {
    return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

在泛型中使用类类型

function create<T>(c: {new(): T; }): T { // 也可以 (c: new () => T):
    return new c();
}

枚举

随便写几笔:

数字枚举定义

默认0开始,没有默认的就自增长:

enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}

访问

enum Response {
    No = 0,
    Yes = 1,
}

function respond(recipient: string, message: Response): void {
    // ...
}

respond("Princess Caroline", Response.Yes)

计算过的成员

后面不能跟默认的:

enum E {
    A = getSomeValue(),
    B, // error! 'A' is not constant-initialized, so 'B' needs an initializer
}

字符串枚举

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

异构枚举

字符串和数字的混合枚举

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

计算的成员和常量成员

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

联合枚举与枚举成员的类型

字面量枚举成员:不带有初始值的常量枚举成员,或者是值被初始化为:

  • 任何字符串字面量(例如: "foo", "bar", "baz")
  • 任何数字字面量(例如: 1, 100)
  • 应用了一元 -符号的数字字面量(例如: -1, -100)

字面量枚举成员的特殊性:

  • 1、枚举成员可以当成类型:
enum ShapeKind {
    Circle,
    Square,
}

interface Circle {
    kind: ShapeKind.Circle; // 被当成类型
    radius: number;
}

let c: Circle = {
    kind: ShapeKind.Square,  // 获取值
    //    ~~~~~~~~~~~~~~~~ Error!
    radius: 100,
}
  • 枚举类型本身变成了每个枚举成员的联合:
enum E {
    Foo,
    Bar,
}

function f(x: E) {
    if (x !== E.Foo || x !== E.Bar) {  // 这个判断毫无意义,x必定为这两个中的一个
        //             ~~~~~~~~~~~
        // Error! Operator '!==' cannot be applied to types 'E.Foo' and 'E.Bar'.
    }
}

运行时的枚举

枚举是一个对象

enum E {
    X, Y, Z
}

function f(obj: { X: number }) {
    return obj.X;
}

// Works, since 'E' has a property named 'X' which is a number.
f(E);

反射映像

数字枚举成员还具有 反向映射,从枚举值到枚举名字。

enum Enum {
    A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

编译成js

var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[a]; // "A"

不会为字符串枚举成员生成反向映射。

const枚举

常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除。不允许计算成员

const enum Enum {
    A = 1,
    B = A * 2
}

外部枚举

只能当作类型。

declare enum Enum {
    A = 1,
    B,
    C = 2
}

类型推论

TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。

基础

如下,x会被推断为数字。

这种推断发生在:

  • 初始化变量、成员
  • 设置默认参数值
  • 决定函数返回值
let x = 3;

最佳通用类型

如果没有最佳通用类型,zoo会被推断为联合类型(Rhino | Elephant | Snake)[]

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

上下文类型

通常在包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句中用到。 上下文类型也会做为最佳通用类型的候选类型:

window.onmousedown = function(mouseEvent) {
    console.log(mouseEvent.button);  //<- Error 因为mouseEvent根据
};

TypeScript类型检查器使用Window.onmousedown函数的类型来推断右边函数表达式的类型。 因此,就能推断出mouseEvent参数的类型了。

函数表达式有明确的参数类型注解,上下文类型被忽略:

window.onmousedown = function(mouseEvent: any) {
    console.log(mouseEvent.button);  //<- Now, no error is given
};

类型兼容性

TypeScript里的类型兼容性是基于结构类型的。 结构类型是一种只使用其成员来描述类型的方式。它正好与名义(nominal)类型形成对比。看如下代码:

interface Named {
    name: string;
}

class Person {
    name: string;
}

let p: Named;
// OK, because of structural typing
p = new Person();

这段代码在名义类型中会报错,因为Person类没有明确说明其实现了Named接口。

规则

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

interface Named {
    name: string;
}

let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };
x = y;

比较两个函数的兼容性

参数

参数少的可以赋值给参数多的。

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

y = x; // OK
x = y; // Error

x的每个参数必须能在y里找到对应类型的参数。参数的名字相同与否无所谓,只看它们的类型。
第二个赋值错误,因为y有个必需的第二个参数,但是x并没有保证有第二个参数,所以不允许赋值。
你可能会疑惑为什么允许忽略参数,像例子y = x中那样。 原因是忽略额外的参数在JavaScript里是很常见的。

返回值

返回值属性多的可以赋值给属性少的。

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

x = y; // OK
y = x; // Error, because x() lacks a location property

类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。

函数参数双向协变

调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式。

enum EventType { Mouse, Keyboard }

interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
    /* ... */
}

// Unsound, but useful and common 这里用了双向协变
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));

// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));

// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));

可选参数及剩余参数(不定参数)

比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。

当一个函数有剩余参数时,它被当做无限个可选参数。

function invokeLater(args: any[], callback: (...args: any[]) => void) {
    /* ... Invoke callback with 'args' ... */
}

// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));

// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));

函数重载

对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。这确保了目标函数可以在所有源函数可调用的地方调用

枚举类型

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

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

let status = Status.Ready;
status = Color.Green;  // Error

比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
}

class Size {
    feet: number;
    constructor(numFeet: number) { }
}

let a: Animal;
let s: Size;

a = s;  // OK
s = a;  // OK

类的私有成员和受保护成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

泛型

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。

如果指定了泛型参数,则按指定后的结果比较:

interface NotEmpty<T> {
    data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y;  // Error, because x and y are not compatible

高级类型

交叉类型

交叉类型是将多个类型合并为一个类型。 例如, Person & Serializable & Loggable同时是 Person 和 Serializable 和 Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员。

联合类型

联合类型表示一个值可以是几种类型之一。 我们用竖线( |)分隔每个类型,所以 number | string | boolean表示一个值可以是 number, string,或 boolean。
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。这很好理解吧。

类型保护与区分类型

如之前提及的,我们只能访问联合类型中共同拥有的成员。所以下面代码会出错:

let pet = getSmallPet();

// 每一个成员访问都会报错
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

为了解决这个问题,可以用类型断言:

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

用户自定义的类型保护

上面的类型断言每次都要写,太繁琐了,可以用类型保护:

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

在这个例子里, pet is Fish就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。
每当使用一些变量调用 isFish时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的:

// 'swim' 和 'fly' 调用都没有问题了

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

typeof类型保护

TypeScript可以将typeof识别为一个类型保护。 也就是说我们可以直接在代码里检查类型了。

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

这些* typeof类型保护*只有两种形式能被识别: typeof v === "typename"和 typeof v !== "typename", "typename"必须是 "number", "string", "boolean"或 "symbol"。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

instanceof类型保护

interface Padder {
    getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(" ");
    }
}

class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value;
    }
}

function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // 类型细化为'StringPadder'
}

null 和 undefined的联合类型

默认情况下,类型检查器认为 null与 undefined可以赋值给任何类型。 --strictNullChecks标记可以:当你声明一个变量时,它不会自动地包含 null或 undefined。 你可以使用联合类型明确的包含它们:

let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以

sn = undefined; // error, 'undefined'不能赋值给'string | null'

TypeScript会把 null和 undefined区别对待。 string | null, string | undefined和 string | undefined | null是不同的类型。

可选参数和可选属性

关于可选参数,如果使用了 --strictNullChecks,可选参数会被自动地加上 | undefined:

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'

可选属性也会有同样的处理:

class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

类型断言和类型保护

由于可以为null的类型是通过联合类型实现,那么你需要使用类型保护来去除 null。 幸运地是这与在JavaScript里写的代码一致:

function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    }
    else {
        return sn;
    }
}

当然也可以这么写:

function f(sn: string | null): string {
    return sn || "default";
}

如果编译器不能够去除 null或 undefined,你可以使用类型断言手动去除。 语法是添加 !后缀: identifier!从 identifier的类型里去除了 null和 undefined:

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet; // error, 'name' is possibly null
  }
  name = name || "Bob";
  return postfix("great");
}

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

类型别名 type

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

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 };

\\ 对比接口
interface Container<T> { value: T }

我们也可以使用类型别名来在属性里引用自己:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<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;

然而,类型别名不能出现在声明右侧的任何地方:

type Yikes = Array<Yikes>; // error

接口 vs. 类型别名

  • 接口创建了一个新的名字,可以在其它任何地方使用。
  • 类型别名不能被 implements(自己也不能 implements其它类型)。
  • 如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

字符串字面量类型

字符串字面量类型:使用固定的字符串:

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! should not pass null or undefined.
        }
    }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here

字符串字面量类型还可以用于区分函数重载:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}

数字字面量类型

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
    // ...
}

很少这样用,但可以缩小调试范围:

function foo(x: number) {
 // 哈哈哈,x = 2的时候满足 x !== 1,x = 1的时候又满足x !== 2,所以恒为true
    if (x !== 1 || x !== 2) {     
        //         ~~~~~~~
        // Operator '!==' cannot be applied to types '1' and '2'.
    }
}

枚举成员类型

如我们在枚举一节里提到的,当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。

在我们谈及“单例类型”的时候,多数是指枚举成员类型和数字/字符串字面量类型,尽管大多数用户会互换使用“单例类型”和“字面量类型”

可辨识联合

3要素:

  • 具有普通的单例类型属性— 可辨识的特征。
  • 一个类型别名包含了那些类型的联合— 联合。
  • 此属性上的类型保护。
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
type Shape = Square | Rectangle | Circle;

使用方式:

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

完整性检查

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
    // should error here - we didn't handle case "triangle"
}

两种解决方式: 首先是启用 --strictNullChecks并且指定一个返回值类型:

function area(s: Shape): number { // error: returns number | undefined
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

第二种方法使用 never类型,编译器用它来进行完整性检查:

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // error here if there are missing cases
    }
}

this类型

和js一样,由于函数调用方式的不同导致this的不同,不详细写了。

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}

索引类型

看一段代码:

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[]

这段代码中包含一些操作符:

首先是 keyof T, 索引类型查询操作符,返回一个联合类型。 对于任何类型 T, keyof T的结果为 T上已知的公共属性名的联合。 例如:

let personProps: keyof Person; // '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的返回值类型会随着你需要的属性改变。

索引类型和字符串索引签名

interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string 由于属性名不是确定的字符串,所以是string
let value: Map<number>['foo']; // number

映射类型

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

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

下面看最简单的映射:

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
// 等同于:
type Flags = {
    option1: boolean;
    option2: boolean;
}

它的语法与索引签名的语法类型,内部使用了 for .. in。 具有三个部分:

  • 类型变量 K,它会依次绑定到每个属性。
  • 字符串字面量联合的 Keys(联合类型),它包含了要迭代的属性名的集合。
  • 属性的结果类型。

通用的映射模板:

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。 例如,假设 Person.name是只读的,那么 Partial<Person>.name也将是只读的且为可选的。

下面是另一个例子, T[P]被包装在 Proxy<T>类里:

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);

用处不小的例子:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
type Record<K extends string, T> = { // K extends string 表示字符串字面量联合类型
    [P in K]: T;
}

Readonly, Partial和 Pick是同态的,但 Record不是。 因为 Record并不需要输入类型来拷贝属性,所以它不属于同态:

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。

typeof 操作符

ts中typeof 操作符可以用来获取一个变量或对象的类型。

interface Person {
  name: string;
  age: number;
}

const sem: Person = { name: "semlinker", age: 30 };
type Sem = typeof sem; // type Sem = Person

Symbols

和js没什么区别,详细的建议去看js相关文档。

迭代器和生成器

和js没什么区别,详细的建议去看js相关文档。

模块

没有发现特别的,跳过。

命名空间

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 }`);
    }
}

分离到多个文件

不写了,参考官网

命名空间和模块

模块解析

声明合并

值得看的,里面有模块扩展。

JSX

装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。装饰器使用 @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时,复合的结果(f ∘ g)(x)等同于f(g(x))。

同样的,在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:

  • 由上至下依次对装饰器表达式求值(求值结果肯定为函数);
  • 求值的结果会被当作函数,由下至上依次调用。

装饰器求值

类中不同声明上的装饰器将按以下规定的顺序应用:

  • 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
  • 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
  • 参数装饰器应用到构造函数。
  • 类装饰器应用到类。

类装饰器

类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)。

类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。

如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。(需要自己处理原型链)

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

// 我们可以这样定义@sealed装饰器:
function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

当@sealed被执行的时候,它将密封此类的构造函数和原型。

一个重载构造函数的例子:

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个参数:

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

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

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不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。)

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

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

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

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个参数:

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

(注意:属性描述符不会做为参数传入属性装饰器,这与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")装饰器是个 装饰器工厂。

参数装饰器

参数装饰器声明在一个参数声明之前(紧靠着参数声明)。参数装饰器应用于类构造函数或方法声明,只能用来监视一个方法的参数是否被传入。参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。

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

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 成员的名字。
  • 参数在函数参数列表中的索引。

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

class Greeter {
    greeting: string;

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

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

元数据 reflect-metadata

看官网。。。

mixins

三斜线指令

看官网啦, /// <reference path="..." />等。

JavaScript文件类型检查

.js和.ts检查的一些区别,以及JSDoc。

完结了,以后想到什么再补充。