TypeScript 学习笔记

1,455 阅读36分钟

TypeScript 学习笔记

基础类型

  • boolean
    • 布尔值
  • number
    • 数字
  • string
    • 字符串
  • number[]Array<number>
    • 数组
  • [string, number]
    • 元组
  • enum
    • 枚举
  • any
    • 未知类型,任意类型
  • void
    • 没有任何类型
  • nullundefined
    • 默认情况下是所有类型的子类型
    • 启用 --strictNullChecks 后只能赋值给 void 和自身
  • never
    • 永远不存在的值的类型
    • 任何类型的子类型,没有类型是 never 的子类型
  • object
    • 非原始类型
  • param as string<string>param
    • 类型断言

声明

  • let
  • const

变量

let [first, second] = [1, 2]
;[second, first] = [first, second]
const [first, ...rest] = [1, 2, 3, 4]
const [first] = [1, 2, 3, 4]
const [, second, , fourth] = [1, 2, 3, 4]
const o = {
    a: 1,
    b: 2,
    c: 3
}
const { a: newName1, b: newName2 }: { a: number; b: number } = o
console.log(newName1, newName2) // >>> 1, 2

函数

function fn([first, second]: [number, number]) {
	console.log(first, second)
}
fn([1, 2]) // >>> 1, 2
type C = { a: number, b?: number }
function fn({ a, b }: C): void {
	console.log(a, b)
}
fn({ a: 1, b: 2 }) // >>> 1, 2
fn({ a: 1 })       // >>> 1, undefined
function fn({ a = 1, b = 2 } = {}): void {
    console.log(a, b)
}
fn() // >>> 1, 2
type C = { a: number; b?: number }
function fn({ a, b = 4 }: C = { a: 3 }): void {
    console.log(a, b)
}
fn({ a: 1, b: 2 }) // >>> 1, 2
fn({ a: 1 })       // >>> 1, 4
fn()               // >>> 3, 4

展开

const arr1 = [1, 2]
const arr2 = [3, 4]
const arr3 = [0, ...arr1, ...arr2, 5]
console.log(arr3) // >>> [0, 1, 2, 3, 4, 5]
const obj1 = { a: 1, b: 2 }
const obj2 = { ...obj1, a: 3 }
console.log(obj2) // >>> { a: 3, b: 2 }
const obj1 = Object.defineProperties(
    { a: 1 },
    {
        b: {
            value: 2,
            enumerable: false
        }
    }
)
const obj2 = { ...obj1 }
console.log(obj1.a, obj1.b) // >>> 1 2
console.log(obj2.a, obj2.b) // >>> 1 undefined

函数

函数类型

function add(x: number, y: number): number {
    return x + y
}
// 推断类型
const myAdd = function(x: number, y: number): number {
    return x + y
}
// 完整函数类型写法
const myAdd: (x: number, y: number) => number = function(x: number, y: number): number {
    return x + y
}
// 参数类型匹配即可,而不在乎参数名
const myAdd: (a: number, b: number) => number = function(x: number, y: number): number {
    return x + y
}

可选参数 和 默认参数

// 可选参数必须跟在必须参数后面
function buildName(firstName: string, lastName?: string) {}
// 可选参数与末尾的默认参数共享参数类型
const buildName1: (a: string, b?: string) => void = (firstName: string, lastName?: string): void => {
    console.info(firstName + ' ' + lastName)
}
const buildName2: (a: string, b?: string) => void = (firstName: string, lastName = 'Smith'): void => {
    console.info(firstName + ' ' + lastName)
}
buildName1('Bob', 'Adams') // >>> Bob Adams
buildName1('Bob')          // >>> Bob undefined
buildName2('Bob', 'Adams') // >>> Bob Adams
buildName2('Bob')          // >>> Bob Smith
// 带默认值的参数不需要放在必须参数的后面
function buildName(firstName: string = 'Will', lastName: string) {
    console.log(firstName + ' ' + lastName)
}
buildName('Bob', 'Adams')     // >>> Bob Adams
buildName(undefined, 'Adams') // >>> Will Adams

剩余参数

// 剩余参数会被当作个数不限的可选参数
function buildName(firstName: string, ...restOfName: string[]): void {
    console.log(firstName + ' ' + restOfName.join(' '))
}
const buildNameFn: (fname: string, ...rest: string[]) => void = buildName
buildNameFn('Joseph', 'Samuel', 'Lucas', 'Mackinzie') // >>> Joseph Samuel Lucas Mackinzie
buildNameFn('Joseph')                                 // >>> Joseph

this

const deck = {
    suits: ['hearts', 'spades', 'clubs', 'diamonds'],
    cards: Array(52),
    createCardPicker() {
        // 箭头函数能保存函数创建时的 this 值,而不是调用时的值
        return () => {
            const pickedCard = Math.floor(Math.random() * 52)
            const pickedSuit = Math.floor(pickedCard / 13)
            return {
                suit: this.suits[pickedSuit], // this: any ==> tsc --noImplicitThis
                card: pickedCard % 13
            }
        }
    }
}
const cardPicker = deck.createCardPicker()
const pickedCard = cardPicker()
console.log(pickedCard) // >>> { suit: 'hearts', card: 11 }
interface Card {
    suit: string
    card: number
}
interface Deck {
    suits: string[]
    cards: number[]
    // 提供一个显式的 this 参数( this 参数是个假的参数,它出现在参数列表的最前面)
    // createCardPicker 期望在某个 Deck 对象上被调用
    createCardPicker(this: Deck): () => Card
}
const deck: Deck = {
    suits: ['hearts', 'spades', 'clubs', 'diamonds'],
    cards: Array(52),
    createCardPicker(this: Deck) {
        return () => {
            const pickedCard = Math.floor(Math.random() * 52)
            const pickedSuit = Math.floor(pickedCard / 13)
            return {
                suit: this.suits[pickedSuit], // this: Deck
                card: pickedCard % 13
            }
        }
    }
}

重载

// 在定义重载的时候,一定要把最精确的定义放在最前面(参数和返回值类型)
function reverse(x: number): number // 重载列表项1
function reverse(x: string): string // 重载列表项2
function reverse(x: number | string): number | string { // 重载实现
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''))
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('')
    }
}
console.log(reverse(1234))   // >>> 4321
console.log(reverse('abcd')) // >>> dcba

继承

class Animal {
    move() {
        console.log('Animal move')
    }
}
class Snake extends Animal {
}
class Horse extends Animal {
    move() {
        console.log('Horse move')
    }
}
const sam = new Snake()
const tom: Animal = new Horse()
sam.move() // >>> Animal move
tom.move() // >>> Horse move

修饰符

class Animal {
    static num: number = 100
    static isAnimal(a: any): boolean {
        return a instanceof Animal
    }
    // 参数属性:构造函数参数添加访问限制符
    constructor(protected name: string) {}
    print() {
        console.log(`print: ${this.nickName}`)
    }
    // 存取器es5以上才支持 tsc --target es5
    get nickName(): string {
        return `Little ${this.name}`
    }
    set nickName(value: string) {
        this.name = value
    }
}
class Snake extends Animal {
    constructor(name: string, private _age: number = 1) {
        // 子类构造函数必须调用 super() ,且在访问 this 属性之前
        super(name)
        this.name = `Snake - ${name} - ${this._age}`
    }
}
class Horse extends Animal {
    private _age: number = 2
    readonly legs: number = 4
    // 子类可以重写父类的方法
    print() {
        console.log(`print: Horse - ${this.name} - ${this._age} - ${this.legs}`)
        super.print()
    }
}
const sam: Animal = new Snake('sam', 3)
const tom = new Horse('tom')
sam.print()                       // >>> print: Little Snake - sam - 3
tom.print()                       // >>> print: Horse - tom - 2 - 4 \n print: Little tom
sam.nickName = 'xyz'
sam.print()                       // >>> print: Little xyz
console.log(Animal.num)           // >>> 100
console.log(Animal.isAnimal(sam)) // >>> true

抽象类

abstract class Animal {
    // 抽象类不能被实例化
    constructor(public name: string) {}
    // 抽象方法只能出现在抽象类中
    abstract move(): void // 方法签名必须在派生类中实现
    // 抽象类可以包含方法实现
    print() {
        console.log(`${this.name} print`)
    }
}
class Dog extends Animal {
    move() {
        console.log(`${this.name} move`)
    }
}
const tom = new Dog('tom')
tom.move()  // >>> tom move
tom.print() // >>> tom print

类的构造函数

class Greeter {
    static standardGreeting: string = 'Hello, there!'
    greeting: string = ''
    greet() {
        if (this.greeting) {
            console.log(`Hello, ${this.greeting}`)
        } else {
            console.log(Greeter.standardGreeting)
        }
    }
}
// 1) 声明 Greeter 类的实例的类型是 Greeter
// 2) 创建了一个叫做构造函数的值(包含了类的所有静态属性)
let greeter1: Greeter
// 创建类的实例,调用构造函数
greeter1 = new Greeter()
greeter1.greet()                      // >>> Hello, there!
// typeof Greeter ==> 取 Greeter 类的类型(而不是实例的类型) | Greeter 标识符的类型 | 构造函数的类型
let greeterMaker: typeof Greeter = Greeter
// 这个类型包含了类的所有静态成员和构造函数
greeterMaker.standardGreeting = 'Hey there!'
let greeter2: Greeter = new greeterMaker()
greeter2.greet()                      // >>> Hey there!
console.log(greeterMaker === Greeter) // >>> true

把类当作接口使用

class Point {
    x: number
    y: number
}
interface Point3d extends Point {
    z: number
}
let point3d: Point3d = { x: 1, y: 2, z: 3 }

混入 Mixins

// 通过可重用组件创建类
// Disposable Mixin
class Disposable {
    isDisposed: boolean = false
    dispose() {
        this.isDisposed = true
    }
}
// Activatable Mixin
class Activatable {
    isActive: boolean = false
    activate() {
        this.isActive = true
    }
    deactivate() {
        this.isActive = false
    }
}
// 混入(把类当成接口使用,仅使用其类型而非实现)
class SmartObject implements Disposable, Activatable {
    constructor() {
        setInterval(() => console.log(`${this.isActive} : ${this.isDisposed}`), 5e2)
    }
    interact() {
        this.activate()
    }
    // 以下为占位属性
    // Disposable
    isDisposed: boolean = false
    dispose: () => void
    // Activatable
    isActive: boolean = false
    activate: () => void
    deactivate: () => void
}
// 帮助函数
function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            // 遍历 mixins 上的所有属性,并复制到目标上去。把占位属性替换成真正的实现代码
            derivedCtor.prototype[name] = baseCtor.prototype[name]
        })
    })
}

applyMixins(SmartObject, [Disposable, Activatable])
const smartObj = new SmartObject()
setTimeout(() => smartObj.interact(), 1e3) // >>> false: false >>> true: false >>> ...

类类型兼容

class Animal {
    constructor(public name: string) { }
}
class Rhino extends Animal {
    constructor() { super('Rhino') }
}
class Employee {
    constructor(public name: string) { }
}
let animal = new Animal('Goat');
let rhino = new Rhino();
let employee = new Employee('Bob');
animal = rhino    // OK
// TypeScript使用的是结构性类型系统。
// 当比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的
animal = employee // OK
class Animal {
    constructor(private name: string) { }
}
class Rhino extends Animal {
    constructor() { super('Rhino') }
}
class Employee {
    constructor(private name: string) { }
}
let animal = new Animal('Goat');
let rhino = new Rhino();
let employee = new Employee('Bob');
animal = rhino
// 然而,当比较带有 private 或 protected 成员的类型的时候,情况就不同了。
// 如果其中一个类型里包含一个 private 或 protected 成员,那么只有当另外一个类型中也存在这样一个 private 成员,
// 并且它们都是来自同一处声明时,才认为这两个类型是兼容的。
animal = employee // >>> error: 不能将类型“Employee”分配给类型“Animal”。类型具有私有属性“name”的单独声明

接口

方便数据结构的类型检查;
为类型命名和为代码定义契约;
关注数据的结构(值的外形)

变量

interface Person {
    name: string
    age?: number // 可选属性
    readonly id: number // 只读属性,对象创建的时候赋值
}
function fn(p: Person) {
    console.log(p.name)
}
let tom: Person = { name: 'Tom', id: 1 }
let will = { name: 'Will', id: 2, sex: 'male' } // 对象字面量赋值给一个变量,不会经过额外的属性检查
fn(tom)
fn(will)
// 对象字面量会被特殊对待且会经过额外的属性检查,使用类型断言绕开这些检查
fn({ name: 'Jack', id: 3, sex: 'male' } as Person)
let a: number[] = [1, 2, 3, 4]
let ro: ReadonlyArray<number> = a
a = ro as number[]
// 函数类型:定义一个调用签名
interface SearchFunc {
    (source: string, subString: string): boolean
}
let mySearch: SearchFunc
// 函数类型的类型检查,函数的参数名不需要与接口里定义的名字相匹配
mySearch = (src: string, sub: string): boolean => {
    return src.search(sub) !== -1
}
console.log(mySearch('xyz', 'y'))
// 可索引类型:描述了对象索引的类型,还有相应的索引返回值类型
// 支持两种索引签名:字符串和数字
// 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型
class Animal {
    name: string = 'aaa'
}
class Dog extends Animal {
    breed: string = 'xxx'
}
interface AnimalDictionary {
    d: Animal // 一旦定义了可索引类型,那么确定属性和可选属性的类型都必须是其返回值类型的子类型
    readonly [x: number]: Dog // 将索引签名设置为只读,防止给索引赋值
    [x: string]: Animal
}
let anm = new Animal()
let dog = new Dog()
dog.name = 'ddd'
let ad: AnimalDictionary = {
    d: anm,
    a: anm,
    1: dog
}
console.log(ad['d'].name) // >>> aaa
console.log(ad['a'].name) // >>> aaa
console.log(ad[1].name)   // >>> ddd
console.log(ad['1'].name) // >>> ddd

// 类实现接口,描述类的公共部分
interface ClockInterface {
    currentTime: Date
    setTime(d: Date): void
}
class Clock implements ClockInterface {
    currentTime: Date = new Date()
    setTime(d: Date): void {
        this.currentTime = d
    }
}
let c: Clock = new Clock()
console.log(c.currentTime.toLocaleString())     // >>> 2019-10-10 5:57:23 PM
setTimeout(() => {
    c.setTime(new Date())
    console.log(c.currentTime.toLocaleString()) // >>> 2019-10-10 5:57:24 PM
}, 1e3)
// 类的静态部分与实例部分
// 为实例方法所用
interface ClockInterface {
    tick(): void
}
// 为构造函数所用
interface ClockConstructor {
    new(h: number, m: number): ClockInterface
}
// 当一个类实现了一个接口时,只对其实例部分进行类型检查
class DigitalClock implements ClockInterface {
    // constructor 存在于类的静态部分,所以不在检查的范围内
    constructor(h: number, m: number) {}
    tick() {
        console.log('beep beep')
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) {}
    tick() {
        console.log('tick tock')
    }
}
// 函数工厂,用传入的类型创建实例
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    // ClockConstructor 类型作为参数传入时,会检查 ctor 是否符合构造函数签名
    return new ctor(hour, minute)
}
const digital = createClock(DigitalClock, 12, 17)
const analog = createClock(AnalogClock, 7, 32)
digital.tick() // >>> beep beep
analog.tick()  // >>> tick tock
// 接口继承类
// 当接口继承了一个类类型时,它会继承类的成员但不包括其实现
// 当一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现
// >> 类类型兼容 <<
class Control {
    private state: any
}
interface SelectableControl extends Control {
    select(): void
}
class Button extends Control implements SelectableControl {
    select() { }
}
class TextBox extends Control {
    select() { }
}
// >>> error: 类“Images”错误实现接口“SelectableControl”。类型具有私有属性“state”的单独声明
class Images implements SelectableControl {
    private state: any
    select() { }
}
// 只有 Control 的子类才拥有一个声明于 Control 的私有成员 state ,这对私有成员的兼容性是必需的
// 实际上, SelectableControl 接口和拥有 select 方法的 Control 类是一样的(相当于混入带私有或受保护成员的类)

接口

// 接口继承接口
interface Shape {
    color: string
}
interface PenStroke {
    penWidth: number
}
interface Square extends Shape, PenStroke {
    sideLength: number
}
let square = <Square>{} // {} as Square
square.color = 'blue'
square.sideLength = 10
square.penWidth = 5
console.log(square) // >>> { color: 'blue', sideLength: 10, penWidth: 5 }
// 混合类型
interface Counter {
    (start: number): string
    interval: number
    reset(): void
}
function getCounter(): Counter {
    let counter = <Counter>function(start: number) {
        console.log(start)
    }
    counter.interval = 123
    counter.reset = function() {
        console.log('reset')
    }
    return counter
}
let c = getCounter()
c(10)
c.reset()
c.interval = 5

泛型

类型变量(只用于表示类型而不是值),当作任意或所有类型来使用

泛型变量

function identity<T>(arg: T): T {
    return arg
}
// 明确传入类型
let output1 = identity<string>('myString')
// 利用类型推论简写
let output2 = identity('myString')
console.log(output1, output2) // >>> myString myString

泛型类型

function identity<T>(arg: T): T {
    return arg
}
// 泛型参数在数量上和使用方式上能对应即可(比函数声明的前面多一个类型参数)
let myIdentity1: <U>(arg: U) => U = identity
// 还可以使用带有调用签名的对象字面量来定义
let myIdentity2: { <U>(arg: U): U } = identity
console.log(myIdentity1<string>('myString')) // >>> myString
console.log(myIdentity2<string>('myString')) // >>> myString

泛型接口

interface GenericIdentityFn {
    <T>(arg: T): T
}
function identity<T>(arg: T): T {
    return arg
}
let myIdentity: GenericIdentityFn = identity
myIdentity<string>('myString')
interface GenericIdentityFn<T> {
    (arg: T): T
}
function identity<T>(arg: T): T {
    return arg
}
let myIdentity: GenericIdentityFn<string> = identity
myIdentity('myString')

泛型类

class GenericNumber<T> {
    zeroValue: T
    add: (x: T, y: T) => T
}
let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = (x, y) => x + y
console.log(myGenericNumber.add(myGenericNumber.zeroValue, 1)) // >>> 1

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

泛型约束

interface Lengthwise {
    length: number
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length)
    return arg
}
loggingIdentity({ length: 3, value: 10 }) // >>> 3
class Animal {
    print() { }
}
// 使用泛型创建工厂函数时,需要引用构造函数的类类型
function create<T>(c: { new(): T }): T {
    return new c()
}
create(Animal).print()
class BeeKeeper {
    hasMask: boolean = true
}
class ZooKeeper {
    nametag: string = 'xxx'
}
class Animal {
    numLegs: number = 4
}
class Bee extends Animal {
    keeper: BeeKeeper = new BeeKeeper()
}
class Lion extends Animal {
    keeper: ZooKeeper = new ZooKeeper()
}
function createInstance<T extends Animal>(c: new () => T): T {
    return new c()
}
createInstance(Lion).keeper.nametag
createInstance(Bee).keeper.hasMask

枚举

数字

// 完全不使用初始化器
enum Direction {
    Up,   // 0
    Down, // 1
    Left, // 2
    Right // 3
}
console.log(Direction['Down'])            // 正向映射 >>> 1
console.log(Direction[Direction['Down']]) // 反向映射 >>> Down
/*
var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
    Direction[Direction["Left"] = 2] = "Left";
    Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
 */
// 不带初始化器的枚举或者被放在第一的位置,或者被放在使用了数字常量或其它常量初始化了的枚举后面
enum Direction {
    Up = 1,   // 1 (初始化器)
    Down,     // 2
    Left = 5, // 5 (初始化器)
    Right     // 6
}
/*
var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 1] = "Up";
    Direction[Direction["Down"] = 2] = "Down";
    Direction[Direction["Left"] = 5] = "Left";
    Direction[Direction["Right"] = 6] = "Right";
})(Direction || (Direction = {}));
 */

字符串

enum Direction {
    Up = 'up',
    Down = 'down',
    Left = 'left',
    Right = 'right'
}
console.log(Direction['Up']) // >>> up
/* 
var Direction;
(function (Direction) {
    Direction["Up"] = "up";
    Direction["Down"] = "down";
    Direction["Left"] = "left";
    Direction["Right"] = "right";
})(Direction || (Direction = {}));
 */

异构枚举

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = 'YES'
}
/* 
var BooleanLikeHeterogeneousEnum;
(function (BooleanLikeHeterogeneousEnum) {
    BooleanLikeHeterogeneousEnum[BooleanLikeHeterogeneousEnum["No"] = 0] = "No";
    BooleanLikeHeterogeneousEnum["Yes"] = "YES";
})(BooleanLikeHeterogeneousEnum || (BooleanLikeHeterogeneousEnum = {}));
 */
enum Days {
    Sat = 1,
    Sun,
    Mon = 1,
    Tue,
    Wed = 'Wed',
    Thu = 4.5,
    Fri
}
/* 
var Days;
(function (Days) {
    Days[Days["Sat"] = 1] = "Sat";
    Days[Days["Sun"] = 2] = "Sun";
    Days[Days["Mon"] = 1] = "Mon";
    Days[Days["Tue"] = 2] = "Tue";
    Days["Wed"] = "Wed";
    Days[Days["Thu"] = 4.5] = "Thu";
    Days[Days["Fri"] = 5.5] = "Fri";
})(Days || (Days = {}));
 */

枚举成员

常量 或 计算出来的

enum Color {
    // 常量
    Initial,               // 枚举的第一个成员且没有初始化器,默认 0
    Inherit,               // 没有初始化器且它之前的枚举成员是一个数字常量,值为它上一个枚举成员的值加1
    Unset = 999,           // 数字常量
    // 常量枚举表达式(可以在编译阶段求值)
    // Black = 'black',    // 含字符串值成员的枚举中不能使用计算值
    currentColor = 1 + 1,  // 一个枚举表达式字面量(数字字面量)
    Transparent = -3,      // 一元运算符 + - ~
    Red = 0xff << 8,       // 二元运算符 <<, >>, >>>
    Green = 0xff << 4,
    Blue = 0xff,
    Redness = Red,         // 一个对之前定义的常量枚举成员的引用(可以是在不同的枚举类型中定义的)
    Yellow = Red | Green,  // 二元运算符 & | ^
    White = Yellow + Blue, // 二元运算符 + - * / %
    Cyan = (Green + Blue), // 括号 ()
    // 需要计算得出的值(所有其它情况)
    PI = Math.PI,
    Len = 'Red'.length
}
/*
var Color;
(function (Color) {
    Color[Color["Initial"] = 0] = "Initial";
    Color[Color["Inherit"] = 1] = "Inherit";
    Color[Color["Unset"] = 999] = "Unset";
    Color[Color["currentColor"] = 2] = "currentColor";
    Color[Color["Transparent"] = -3] = "Transparent";
    Color[Color["Red"] = 65280] = "Red";
    Color[Color["Green"] = 4080] = "Green";
    Color[Color["Blue"] = 255] = "Blue";
    Color[Color["Redness"] = 65280] = "Redness";
    Color[Color["Yellow"] = 65520] = "Yellow";
    Color[Color["White"] = 65775] = "White";
    Color[Color["Cyan"] = 4335] = "Cyan";
    Color[Color["PI"] = Math.PI] = "PI";
    Color[Color["Len"] = 'Red'.length] = "Len";
})(Color || (Color = {}));
 */

当所有枚举成员都拥有字面量枚举值时,枚举成员成为了类型,枚举类型成为成员的联合类型

enum ShapeKind {
    Circle,
    Square
}
interface Circle {
    kind: ShapeKind.Circle
    radius: number
}
interface Square {
    kind: ShapeKind.Square
    sideLength: number
}
const c: Circle = {
    kind: ShapeKind.Square, // error: 不能将类型“ShapeKind.Square”分配给类型“ShapeKind.Circle”
    radius: 100
}

运行时的枚举:在运行时是真正存在的对象

enum E {
    Foo,
    Bar,
    Baz
}
function f(x: { Foo: number }) {
    return x.Foo
}
console.log(f(E)) // >>> 0

常量枚举

能使用常量枚举表达式(不能包含计算成员),并且在编译阶段会被删除

const enum Enum {
    A = 2,
    B = A * 2
}
console.log(Enum['A'])
console.log(Enum['B'])
// 常量枚举成员在使用的地方会被内联进来
// console.log(2 /* 'A' */);
// console.log(4 /* 'B' */);

外部枚举

用来描述已经存在的枚举类型的形状

declare enum Direction {
    Up = 1,
    Down = 3,
    Left,
    Right
}
const directions = [Direction.Up, Direction.Down, Direction.Left, Direction.Right]
// var directions = [Direction.Up, Direction.Down, Direction.Left, Direction.Right]
declare const enum Direction {
    Up = 1,
    Down = 3,
    Left,
    Right
}
const directions = [Direction.Up, Direction.Down, Direction.Left, Direction.Right]
// var directions = [1 /* Up */, 3 /* Down */, 4 /* Left */, 5 /* Right */];

对于非常数的外部枚举而言,会被当做需要经过计算的

类型兼容性

结构化类型系统

比较对象

interface Named {
    name: string
}
class Person {
    name: string = 'xx'
}
let p: Named
p = new Person()
console.log(p) // Person { name: 'xx' }
let x: Named
let y = { name: 'Alice', location: 'Seattle' }
x = y
console.log(x) // { name: 'Alice', location: 'Seattle' }
// 如果x要兼容y,那么y至少具有与x相同的属性(结构上:y>=x)
function greet(x: Named) {
    console.log(x)
}
greet(y)       // { name: 'Alice', location: 'Seattle' }

比较函数

  1. 参数列表
let x = (a: number) => 0
let y = (b: number, s: string) => 0
y = x // ok
x = y // error

x 的每个参数必须能在 y 里找到对应类型的参数, x 才能赋值给 y

  1. 返回值类型
let x = () => ({ name: 'Alice' })
let y = () => ({ name: 'Alice', location: 'Seattle' })
x = y // ok
y = x // error

y 返回值至少具有与 x 相同的属性, y 才能赋值给 x

函数参数双向协变

当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功
能够实现很多JavaScript里的常见模式

enum EventType {
    Mouse,
    Keyboard
}
interface Event {
    timestamp: number
}
interface MoseEvent extends Event {
    x: number
    y: number
}
interface KeyEvent extends Event {
    keyCode: number
}
function listenEvent(eventType: EventType, handler: (e: Event) => void) {
}
listenEvent(EventType.Mouse, (e: MoseEvent) => console.log(e.x + ', ' + e.y))
listenEvent(EventType.Mouse, (e: Event) => console.log((<MoseEvent>e).x + ', ' + (<MoseEvent>e).y))
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MoseEvent) => console.log(e.x + ', ' + e.y)))
// listenEvent(EventType.Mouse, (e: number) => console.log(e)) // error

可选参数 和 剩余参数

额外的可选参数不是错误(剩余参数,相当于任意多个可选参数)

let x1 = (a: number) => 0
let y1 = (b: number, s?: string) => 0
y1 = x1
let x2 = (a: number, s?: string) => 0
let y2 = (b: number) => 0
y2 = x2
function invokeLater(args: any[], callback: (...args: any[]) => void) {
}
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y))
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y))

函数重载

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

枚举

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

enum Color {
    Red
}
enum BgColor {
    Red
}
let c = Color.Red
c = BgColor.Red // error: 不能将类型“BgColor”分配给类型“Color”

类与对象字面量和接口差不多,但有一点不同:类是有静态部分和实例部分的类型。
比较两个类类型的对象时,只有实例的成员会被比较。
静态成员和构造函数不在比较的范围内。

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

类的私有成员和受保护成员:

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

class Animal {
    private feet: number
    numFeet: string
}
class Dog extends Animal {
}
class Size {
    private feet: number
    numFeet: string
}
let a: Animal
let d: Dog
let s: Size
a = d // ok
d = a // ok
s = d // error: 不能将类型“Dog”分配给类型“Size”。类型具有私有属性“feet”的单独声明。

泛型

类型参数只影响使用其做为类型一部分的结果类型

interface Empty<T> {
}
let x1: Empty<string>
let y1: Empty<number>
x1 = y1 // ok

interface NotEmpty<T> {
    data: T
}
let x2: NotEmpty<string>
let y2: NotEmpty<number>
x2 = y2 // error

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较

let identity = function <T>(x: T): T {
    return x
}
let reverse = function <U>(y: U): U {
    return y
}
identity = reverse

高级类型

交叉类型 A & B

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{}
    for (let id in first) {
        (result as any)[id] = (first as any)[id]
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id]
        }
    }
    return result
}
class Person {
    constructor(public name: string) { }
}
interface Loggable {
    log(): void
}
class ConsoleLogger implements Loggable {
    log() {
        console.log('ConsoleLogger')
    }
}
const jim = extend(new Person('Jim'), new ConsoleLogger())
console.log(jim.name) // >>> Jim
jim.log()             // >>> ConsoleLogger

联合类型 A | B

function padLeft(value: string, padding: number | string) {
    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}'.`)
}
console.log(padLeft('abc', 4))           // >>>     abc
console.log(padLeft('abc', 'xxxx'))      // >>> xxxxabc
// tsc --strictNullChecks
console.log(padLeft('hello', undefined)) // >>> Error: Expected string or number, got 'undefined'.
console.log(padLeft('hello', null))      // >>> Error: Expected string or number, got 'null'.

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员

interface Bird {
    fly()
    layEggs()
}
interface Fish {
    swim()
    layEggs()
}
function getSmallPet(): Fish | Bird {
    return {
        fly() { },
        layEggs() { }
    }
}
let pet = getSmallPet()
pet.layEggs()
if ((<Fish>pet).swim) {
    (<Fish>pet).swim()
}
else {
    (<Bird>pet).fly()
}

类型保护 和 区分类型

  • 用户自定义的类型保护
// 返回值 pet is Fish 就是类型谓词
function isFirsh(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined
}
// ...
if (isFirsh(pet)) {
    // 每当使用一些变量调用 isFish 时,TypeScript 会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。
    pet.swim()
} else {
    // 一旦检查过类型,就能在之后的每个分支里清楚地知道 pet 的类型
    pet.fly()
}
  • typeof 类型保护
function isNumber(x: any): x is number {
    return typeof x === 'number'
}
function isString(x: any): x is string {
    return typeof x === 'string'
}
function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(' ') + value
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`)
}

不必将 typeof x === 'number' 抽象成一个函数,因为 TypeScript 可以将它识别为一个类型保护。
也就是说我们可以直接在代码里检查类型。

function padLeft(value: string, padding: string | number) {
    if (typeof padding === 'number') {
        return Array(padding + 1).join(' ') + value;
    } else { // padding: string
        return padding + value;
    }
}
console.log(padLeft('hello', undefined)) // >>> undefinedhello

只有两种形式能被识别为 typeof 类型保护:

typeof v === 'typename'typeof v !== 'typename'
'typename' 必须是 'number''string''boolean''symbol' (如果是其他字符串,则不会识别为类型保护)。

  • instanceof 类型保护

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('ssss')
}
// 类型为 SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder()
if (padder instanceof SpaceRepeatingPadder) {
    // 类型细化为 'SpaceRepeatingPadder'
    console.log(padder.getPaddingString() + 'Hello!') // >>>     Hello!
} else if (padder instanceof StringPadder) {
    // 类型细化为 'StringPadder'
    console.log(padder.getPaddingString() + 'Hello!') // >>> ssssHello!
}

可以为 null 的类型

  • 可选参数 和 可选属性

使用了 --strictNullChecks,可选参数会被自动地加上 | undefined

// y: number | undefined
function f(x: number, y?: number) {
    return x + (y || 0);
}
class C {
    a: number
    b?: number // => b: number | undefined
}
  • null 类型保护和类型断言 !
function f(sn: string | null): string {
    if (sn == null) {
        return 'default'
    } else {
        return sn
    }
}
function f(sn: string | null): string {
    return sn || 'default'
}

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

function broken(name: string | null): string {
    function postfix(epithet: string) {
        return name.charAt(0) + '. the' + epithet // >>> error: 对象可能为 null
    }
    name = name || 'Bob'
    return postfix('great')
}
function fixed(name: string | null): string {
    function postfix(epithet: string) {
        return name!.charAt(0) + '. the' + epithet // *name!.charAt(0)*
    }
    name = name || 'Bob'
    return postfix('great')
}

类型别名 type

// 不会新建一个类型 - 创建了一个新名字来引用那个类型
type Name = string
type NameResolver = () => string
type NameOfResolver = Name | NameResolver
function getName(n: NameOfResolver): Name {
    if (typeof n === 'string') {
        return n
    } else {
        return n()
    }
}
// 泛型
type 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 }
let p1 = {} as LinkedList<Person>
let p2 = {} as LinkedList<Person>
p1.name = 'p1'
p2.name = 'p2'
p1.next = p2
console.log(p1.name, p1.next.name) // >>> p1 p2

类型别名没有创建新的类型,不能 extendsimplements。应该尽量去使用接口代替类型别名

字符串字面量类型

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 {
        }
    }
}
let button = new UIElement()
button.animate(0, 0, 'ease-in')
// 用于区分函数重载
function createElement(tagName: 'img'): HTMLImageElement
function createElement(tagName: 'input'): HTMLInputElement
// ...
function createElement(tagName: string): Element {
    // ...
}
const el: HTMLImageElement = createElement('img')

数字字面量类型

function foo(x: 1 | 2): 2 | 3 {
    if (x !== 1) {
        return 2
    } else {
        return 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 // 联合类型 + 类型别名
// 使用 never 类型,来进行完整性检查
function assertNever(x: never): never {
    throw new Error('Unexpected object: ' + x)
}
function area(s: Shape): number {
    switch (s.kind) {
        case 'square': return s.size ** 2 // 类型保护
        case 'rectangle': return s.width * s.height
        case 'circle': return Math.PI * s.radius ** 2
        // 如果 case 没有包含所有场景:
        // 1. 编译器会认为函数可能会返回 undefined,这和函数返回值 number 不符,从而产生提示
        // 2. assertNever 函数参数 s 会具有一个真实的类型,从而得到一个错误提示
        default: return assertNever(s)
    }
}
console.log(area({ kind: 'square', size: 4 })) // >>> 16

多态的 this 类型

class BasicCalculator {
    constructor(protected value: number = 0) { }
    currentValue() {
        return this.value
    }
    add(operand: number): this {
        this.value += operand
        return this
    }
    multiply(operand: number): this {
        this.value *= operand
        return this
    }
}
console.log(
    new BasicCalculator(2)
        .multiply(5)
        .add(1)
        .currentValue()
) // >>> 111
class ScientificCalculator extends BasicCalculator {
    constructor(value = 0) {
        super(value)
    }
    sin() {
        this.value = Math.sin(this.value)
        return this
    }
}
console.log(
    new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue()
) // >>> 0.4559788891106302

索引类型 extends keyof T

检查使用了动态属性名的代码

// function pluck(o: any, names: string[]) {
//     return names.map(n => o[n])
// }
// 1. keyof T 是索引类型查询操作符,对于任何类型 T, keyof T 的结果为 T 上已知的公共属性名的联合。
// 2. T[K] 是索引访问操作符 
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
}
const p: Person = {
    name: 'Jarid',
    age: 35
}
console.log(pluck(p, ['name'])) // >>> ['Jarid']
// 等价写法 keyof Person 是 Person 上公共属性的联合
let personProps1: keyof Person
let personProps2: 'name' | 'age'
function getProperty<T, K extends keyof T>(o: T, n: K): T[K] {
    return o[n]
}
// 索引类型和字符串索引签名
// keyof T 是 string, T[string] 是索引签名的类型
interface IMap<T> {
    [key: string]: T
}
let keys: keyof IMap<number>   // string | number
let value: IMap<number>['foo'] // number

映射类型 in keyof T

interface Person {
    name: string
    age: number
}
type TReadonly<T> = {
    readonly [P in keyof T]: T[P]
}
type TPartial<T> = {
    [P in keyof T]?: T[P]
}
type ReadonlyPerson = TReadonly<Person>
type PersonPartial = TPartial<Person>
/*
type ReadonlyPerson = {
    readonly name: string;
    readonly age: number;
}
type PersonPartial = {
    name?: string | undefined;
    age?: number | undefined;
}
 */
type Keys = 'option1' | 'option2' // 要迭代的属性名的集合
type Flags = { [K in Keys]: boolean } // 类型变量 K 会依次绑定到每个属性
/*
type Flags = {
    option1: boolean;
    option2: boolean;
}
 */
type NullablePerson = { [P in keyof Person]: Person[P] | null }
type Nullable<T> = { [P in keyof T]: T[P] | null }
type TPick<T, K extends keyof T> = { [P in K]: T[P] }
type TRecord<K extends string, T> = { [P in K]: T }
type ThreeStringProps = TRecord<'prop1' | 'prop2' | 'prop3', string>
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> {
    let result = {} as Proxify<T>
    // ...
    return result
}
let props = { a: 1 }
let proxyProps = proxify(props)
// 拆包(这个拆包推断只适用于同态的映射类型)
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)
  • 预定义的有条件类型
    • Exclude<T, U> -- 从 T 中剔除可以赋值给 U 的类型
    • Extract<T, U> -- 提取 T 中可以赋值给 U 的类型
    • NonNullable<T> -- 从 T 中剔除 nullundefined
    • ReturnType<T> -- 获取函数返回值类型
    • InstanceType<T> -- 获取构造函数类型的实例类型
type T00 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>           // 'b' | 'd'
type T01 = Extract<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>           // 'a' | 'c'
type T02 = Exclude<string | number | (() => void), Function>         // string | number
type T03 = Extract<string | number | (() => void), Function>         // () => void
type T04 = NonNullable<string | number | undefined>                  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined> // (() => string) | string[]
function f1(s: string) {
    return { a: 1, b: s }
}
class C {
    x = 0
    y = 0
}
type T10 = ReturnType<() => string>                               // string
type T11 = ReturnType<(s: string) => void>                        // void
type T12 = ReturnType<(<T>() => T)>                               // unknown
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)> // number[]
type T14 = ReturnType<typeof f1>                                  // { a: number, b: string }
type T15 = ReturnType<any>                                        // any
type T16 = ReturnType<never>                                      // never
type T17 = ReturnType<string>   // Error
type T18 = ReturnType<Function> // Error
type T20 = InstanceType<typeof C> // C
type T21 = InstanceType<any>      // any
type T22 = InstanceType<never>    // never
type T23 = InstanceType<string>   // Error
type T24 = InstanceType<Function> // Error

模块

外部模块
模块是自声明的;两个模块之间的关系是通过在文件级别上使用imports和exports建立的

导出

  • 导出声明: 任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export关键字来导出
  • 导出语句: 可以对导出的部分重命名
  • 重新导出: 不会在当前模块导入那个模块或定义一个新的局部变量
    • 扩展其它模块,并且只导出那个模块的部分内容
    • 一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from 'module'

导入

  • 导入一个模块中的某个导出内容
    • 可以对导入内容重命名
  • 将整个模块导入到一个变量,并通过它来访问模块的导出部分
  • 具有副作用的导入模块
    • 一些模块会设置一些全局状态供其它模块使用 import 'module'

默认导出

  • 每个模块都可以有一个 default 导出
  • 默认导出使用 default 关键字标记;并且一个模块只能够有一个default导出
  • 类和函数声明可以直接被标记为默认导出
  • 标记为默认导出的类和函数的名字是可以省略的
  • default 导出也可以是一个值

export = 和 import = require()

  • export = 兼容 export defaultexports
  • 若使用 export = 导出一个模块,则必须使用TypeScript的特定语法 import module = require('module') 来导入此模块。

可选的模块加载 和 其它高级加载场景

  • 动态模块加载
  • 为了确保类型安全性,我们可以使用 typeof 关键字
    • typeof 关键字,当在表示类型的地方使用时,会得出一个类型值,这里就表示模块的类型

使用其它的 JavaScript 库

需要声明类库所暴露出的 API

  • 外部模块
declare module 'path' {
    export function normalize(p: string): string;
    export function join(...paths: any[]): string;
    export let sep: string;
}
  • 外部模块简写(简写模块里所有导出的类型将是 any
declare module 'qs'
  • 模块声明通配符 *
// shims-vue.d.ts
declare module '*.vue' {
    import Vue from 'vue'
    export default Vue
}
  • UMD 模块
export as namespace foo
// export = foo
// export default foo

创建模块结构指导

  • 尽可能地在顶层导出
  • 如果仅导出单个 classfunction,使用 export default
  • 如果要导出多个对象,把它们放在顶层里导出
    • 当导入的时候:明确地列出导入的名字
    • 当有大量导出内容的时候:使用命名空间导入模式
  • 使用重新导出进行扩展
  • 模块里不要使用命名空间

模块结构上的危险信号

  1. 文件的顶层声明是 export namespace Foo { ... } (删除 namespace Foo 并把所有内容向上层移动一层)
  2. 文件只有一个 export classexport function (考虑使用 export default
  3. 多个文件的顶层具有同样的 export namespace Foo { (不要以为这些会合并到一个 namespace Foo 中!)

命名空间

内部模块

分离到多个文件

  • 加入引用标签来告诉编译器文件之间的关联 /// <reference path='Validation.ts' />
  • 当涉及到多文件时,我们必须确保所有编译后的代码都被加载了
    • 第一种方式:把所有的输入文件编译为一个输出文件,需要使用 --outFile 标记
    • 第二种方式:在页面上通过 <script> 标签把所有生成的JS文件按正确的顺序引进来

别名 import q = x.y.z

使用其它的 JavaScript 库

需要声明类库导出的 API

  • 外部命名空间
// 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 var d3: D3.Base

命名空间 和 模块

使用命名空间

命名空间是位于全局命名空间下的一个普通的带有名字的 JavaScript 对象

使用模块

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

命名空间和模块的陷阱

  • 对模块使用 /// <reference>
  • 不必要的命名空间

模块解析

相对 和 非相对模块导入

  • 相对导入是以 /./../ 开头的
    • 使用相对路径导入自己写的模块
  • 所有其它形式的导入被当作非相对的
    • 使用非相对路径来导入外部依赖

模块解析策略

共有两种可用的模块解析策略: Node 和 Classic

  • Classic
    • 相对导入的模块是相对于导入它的文件进行解析的
    • 对于非相对模块的导入,编译器则会从包含导入文件的目录开始依次向上级目录遍历,尝试定位匹配的声明文件。
  • Node
    • 在运行时模仿 Node.js 模块解析机制

附加的模块解析标记

TypeScript编译器有一些额外的标记用来通知编译器在源码编译成最终输出的过程中都发生了哪个转换。
注意:编译器不会进行这些转换操作;它只是利用这些信息来指导模块的导入。

  • baseUrl
    • tsconfig.json 里的 baseUrl 属性(或者命令行中的 baseUrl 值)
    • 所有非相对模块导入都会被当做相对于 baseUrl
    • 相对模块的导入不会被设置的 baseUrl 所影响,因为它们总是相对于导入它们的文件
  • 路径映射
    • tsconfig.json 里的 paths 来支持这样的声明映射
    • 注意 paths 是相对于 baseUrl 进行解析的
  • 利用 rootDirs 指定虚拟目录

跟踪模块解析

通过 --traceResolution 启用编译器的模块解析跟踪

使用 --noResolve

--noResolve 编译选项告诉编译器不要添加任何不是在命令行上传入的文件到编译列表。
编译器仍然会尝试解析模块,但是只要没有指定这个文件,那么它就不会被包含在内。

为什么在 exclude 列表里的模块还会被编译器使用

要从编译列表中排除一个文件,需要在排除它的同时,还要排除所有对它进行import或使用了 /// <reference path='...' /> 指令的文件
(如果编译器识别出一个文件是模块导入目标,它就会加到编译列表里,不管它是否被排除了)

tsconfig.json 将文件夹转变一个“工程”,如果不指定任何 excludefiles
文件夹里的所有文件包括 tsconfig.json 和所有的子目录都会在编译列表里。
如果想利用 exclude 排除某些文件,甚至想指定所有要编译的文件列表,请使用 files

声明合并

TypeScript中的声明会创建以下三种实体之一:命名空间,类型或值。

Declaration Type Namespace Type Value
Namespace X X
Class X X
Enum X X
Interface X
Type Alias X
Function X
Variable X

合并接口

// 非函数的成员
interface Box {
    height: number
    width: number
    color: string
}
interface Box {
    scale: number
    color: string // 重复成员的类型要一致
}
const box: Box = { height: 5, width: 6, scale: 10, color: 'red' }
// 函数成员
// 每个同名函数声明都会被当成这个函数的一个重载,且后面的接口具有更高的优先级
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 {
    let haveMuscles = true
    export interface Legged { feet: string }
    export class Zebra { }
    export function animalsHaveMuscles() {
        let leg: Legged = {
            feet: 'feet10',
            numOfLegs: 10
        }
        let dog = new Dog()
        return haveMuscles // 非导出成员仅在其原有的(合并前的)命名空间内可见
    }
}
namespace Animals {
    export interface Legged { numOfLegs: number }
    export class Dog { }
    export function doAnimalsHaveMuscles() {
        let leg: Legged = {
            feet: 'feet10',
            numOfLegs: 10
        }
        let zebra = new Zebra()
        // return haveMuscles // error: 找不到名称“haveMuscles”
    }
}
let leg: Animals.Legged = {
    numOfLegs: 1,
    feet: 'feet'
}
let zebra: Animals.Zebra = new Animals.Zebra()
let dog: Animals.Dog = new Animals.Dog()

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

命名空间可以与其它类型的声明进行合并。
只要命名空间的定义符合将要合并类型的定义。
合并结果包含两者的声明类型。
TypeScript 使用这个功能去实现一些 JavaScript 里的设计模式。

  • 合并命名空间和类

给类添加内部类

class Album {
    label = Album.AlbumLabel // 内部类(合并后)
    print() {
        console.log('Album print')
    }
}
namespace Album {
    // 必须导出 AlbumLabel 类,好让合并的类能访问
    export class AlbumLabel {
        print() {
            console.log('Album.AlbumLabel print')
        }
    }
}
let a = new Album()
let l = new a.label()
a.print() // >>> Album print
l.print() // >>> Album.AlbumLabel print
  • 合并命名空间和函数

给函数添加属性

function buildLabel(name: string): string {
    return buildLabel.prefix + name + buildLabel.suffix
}
namespace buildLabel {
    export let suffix = '!'
    export let prefix = 'Hello, '
}
console.log(buildLabel('world')) // >>> Hello, world!
  • 合并命名空间和枚举类型

给枚举添加枚举项

enum Color {
    red = 1,
    green = 2,
    blue = 4
}
namespace Color {
    export function mixColor(colorName: string): number {
        switch (colorName) {
            case 'yellow': return Color.red + Color.green
            case 'white': return Color.red + Color.green + Color.blue
            case 'magenta': return Color.red + Color.blue
            case 'cyan': return Color.green + Color.blue
            default: return Color.red
        }
    }
}
let c1: Color = Color.red
let c2: Color = Color.mixColor('magenta')
console.log(c1, c2) // >>> 1 5

非法的合并

目前,类不能与其它类或变量合并(使用[混入类](#混入 Mixins),模仿类的合并)。

模块扩展

/* observable.ts */
export class Observable<T> { }
// 全局扩展
declare global {
    interface Array<T> {
        // 在模块内部添加声明到全局作用域中
        // 全局扩展与模块扩展的行为和限制是相同的
        toObservable(): Observable<T>
    }
}
Array.prototype.toObservable = function <T>() {
    return this as Observable<T>
}

/* map.ts */
import { Observable } from './observable'
// 模块扩展
declare module './observable' {
    // 当这些声明在扩展中合并时,就好像在原始位置被声明了一样。
    // 但是,不能在扩展中声明新的顶级声明(仅可以扩展模块中已经存在的声明)。
    interface Observable<T> {
        map<U>(f: (x: T) => U): Observable<U>
    }
}
Observable.prototype.map = function <U>(f: <T>(x: T) => U): Observable<U> {
    let result = {} as Observable<U>
    return result
}

/* consumer.ts */
import { Observable } from './observable'
import './map'

let o: Observable<number> = [1.1, 2.2, 3.3]
console.log(o.map(v => v.toFixed())) // >>> ['1', '2', '3']

let a: number[] = [4, 5, 6]
let t = a.toObservable()
console.log(t.map(v => v.toFixed())) // >>> ['4', '5', '6']

装饰器

tsc --target es5 --experimentalDecorators
不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare 的类)里

  • 装饰器函数
function f(target: any) {
    console.log('f is called')
}
@f
class C {
}
// >>> f is called
  • 装饰器工厂
function f(v: string) {
    console.log(`f(${v})`)
    return function (target: any) {
        console.log(target)
    }
}
@f('abc')
class C {
}
// >>> f(abc)
// >>> [Function: C]

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

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

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

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

类装饰器

应用于类的构造函数,可以用来监视,修改或替换类定义

@sealed
@classDecorator
class Greeter {
}
/**
 * @param {Function} constructor - 类的构造函数
 */
function sealed(constructor: Function) {
    console.log('sealed')
    Object.seal(constructor)
    Object.seal(constructor.prototype)
}
// 重载构造函数
function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
    console.log('classDecorator')
    return class extends constructor {
        greeting = 'Hello'
    }
}
const g = new Greeter()
// >>> classDecorator
// >>> sealed
// >>> class_1 { greeting: 'Hello' }

方法装饰器

应用于方法的属性描述符,可以用来监视,修改或替换方法定义

class Greet {
    @nonenumerable
    greet() { return 'greet' }
}
/**
 * @param {any} target                    - 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
 * @param {string} propertyKey            - 成员的名字
 * @param {PropertyDescriptor} descriptor - 成员的属性描述符
 */
function nonenumerable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = false
}

访问器装饰器

应用于访问器的属性描述符,可以用来监视,修改或替换一个访问器的定义(用法同方法装饰器)

class Greet {
    @nonconfigurable
    get x() { return 'x' }
}
/**
 * @param {any} target                    - 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
 * @param {string} propertyKey            - 成员的名字
 * @param {PropertyDescriptor} descriptor - 成员的属性描述符
 */
function nonconfigurable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.configurable = false
}

属性装饰器

只能用来监视类中是否声明了某个名字的属性

class Greeter {
    @format
    greeting: string
}
/**
 * @param {object} target         - 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
 * @param {string} propertyKey    - 成员的名字
 */
function format(target: Object, propertyKey: string | symbol) {}

参数装饰器

应用于类构造函数或方法声明
只能用来监视一个方法的参数是否被传入,参数装饰器的返回值会被忽略

class Greeter {
    greet(@required name: string) {}
}
/**
 * @param {object} target         - 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
 * @param {string} propertyKey    - 成员的名字
 * @param {number} parameterIndex - 参数在函数参数列表中的索引
 */
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {}

JSX

三种JSX模式:preservereactreact-native

模式 输入 输出 输出文件扩展名
preserve <div /> <div /> .jsx
react <div /> React.createElement('div') .js
react-native <div /> <div /> .js

如果是ES6的类,类类型就是类的构造函数和静态部分,实例类型为这个类的实例的类型
如果是个工厂函数,类类型为这个函数,实例类型为这个函数返回值类型

三斜线指令

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

  • /// <reference path='...' />
  • /// <reference types='...' />
  • /// <reference no-default-lib='true'/>
  • /// <amd-module name='...' />
  • /// <amd-dependency path='...' />
    • 注意:这个指令被废弃了。使用 import 'moduleName' 语句代替。

声明文件

  • 全局变量
    • declare var, declare let, declare const
  • 全局函数
    • declare function
  • 带属性的对象
    • declare namespace
  • 函数重载
    • declare function
  • 可重用类型(接口)
    • interface
  • 可重用类型(类型别名)
    • type
  • 组织类型
    • declare namespace Name { interface }, declare namespace Name1.Name2 { interface }
    • declare class
  • 外部枚举
    • declare enum
  • 扩展全局变量
    • declare global
  • 扩展模块
    • declare module
  • 三斜线指令
    • /// <reference />
  • 导出变量
    • export
  • 导出带属性的对象
    • export namespace
  • ES6 默认导出
    • export default
  • commonjs 导出模块
    • export =
  • UMD 库声明全局变量
    • export as namespace

模板

规范

  • 普通类型
    • 使用 number, string, boolean, object 代替 Number, String, Boolean, Oject
  • 回调函数
    • 返回值被忽略时使用 void 代替 any
    • 不使用可选参数
    • 只使用最大参数个数写一个重载
  • 函数重载
    • 顺序
    • 使用可选参数代替参数个数不同的重载
    • 使用联合类型代替某个位置上参数类型不同的重载

tsconfig.json

tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项
当命令行上指定了输入文件时,tsconfig.json 文件会被忽略

  • files
    • 指定一个包含相对或绝对文件路径的列表
  • includeexclude
    • 指定一个文件 glob 匹配模式列表。支持的glob通配符有:
      • * 匹配0或多个字符(不包括目录分隔符)
      • ? 匹配一个任意字符(不包括目录分隔符)
      • **/ 递归匹配任意子目录
  • compilerOptions
    • baseUrl
    • paths
    • rootDirs
    • typeRoots
    • types
    • outDir

在命令行上指定的编译选项会覆盖在 tsconfig.json 文件里的相应选项

参考