TypeScript 笔记

60 阅读25分钟

基本类型

ES6 的数据类型TypeScript 的数据类型
BooleanBoolean
NumberNumber
StringString
ArrayArray
FunctionFunction
ObjectObject
SymbolSymbol
undefinedundefined
void
any
never
元祖
枚举
高级类型

类型注解

作用:相当于强类型语言中的类型声明

语法:(变量/函数): type

// 原始类型
let bool: boolean = true;
let num: number = 123;
let str: string = 'abc';

// 数组
let arr1: number[] = [1, 2, 3];
let arr2: Array<number | string> = [1, 2, 3, '4'];

// 元祖
let tuple: [number, string] = [0, '1'];

// 函数
let add = (x: number, y: number): number => x + y;
let compute: (x: number, y: number) => number;
compute = (a, b) => a + b;

// 对象
let obj: { x: number, y: number } = { x: 1, y: 2 };
obj.x = 3;

// symbol
let s1: symbol = Symbol();
let s2 = Symbol();
console.log(s1 === s2);

// undefined, null
let un: undefined = undefined;
let nu: null = null;

// void
let noReturn = () => { }

// any
let x;
x = 1;
x = [];
x = () => { };

// never
let error = () => {
    throw new Error('error')
}
let endless = () => {
    while (true) { }
}

枚举类型

枚举:一组有名字的常量集合。

// 数字枚举
enum Role {
    Reporter = 1,
    Developer,
    Maintainer,
    Owner,
    Guest
}
console.log(Role.Reporter);
console.log(Role);

// 字符串枚举
enum Message {
    Success = '恭喜你,成功了',
    Fail = '抱歉,失败了'
}

// 异构枚举: 数字和字符串混用。这里 N 默认是 0,Y 为指定字符串。(不建议使用)
enum Answer {
    N,
    Y = 'Yes'
}

// 枚举成员
enum Char {
    // const
    a,
    b = Char.a,
    c = 1 + 3,
    // computed
    d = Math.random(),
    e = '123'.length,

    f = 4,
    g,
    h
}

// 常量枚举
const enum Month {
    Jan,
    Feb,
    Mar
}
let month = [Month.Jan, Month.Feb, Month.Mar];

// 枚举类型
enum E { a, b }
enum F { a = 0, b = 1 }
enum G { a = 'apple', b = 'banana' }

let e: E = 3;
let f: F = 3;
// e === f // Error

let e1: E.a = 1;
let e2: E.b;
// e1 === e2 // Error
let e3: E.a = 1;
// e1 === e3; // Normal

let g1: G = G.b;
let g2: G.a = G.a;
// let g3: G = 'aaa'; // Error
// let g4: G.a = 'aaa'; // Error
// let g4: G.a = 'apple'; // Error
// 数字枚举类型可以赋值任意数字,字符串枚举类型只能赋值枚举的值

接口

对象类型接口

interface List {
    readonly id: number;
    name: string;
    // [x: string]: any
    age?: number
}
interface Result {
    data: List[]
}
function render(result: Result) {
    result.data.forEach((value) => {
        console.log(value.id, value.name)
        if (value.age) {
            console.log(value.age)
        }
        // value.id++;
    })
}
let result = {
    data: [
        { id: 1, name: 'A', sex: 'male' },
        { id: 2, name: 'B' }
    ]
}
render(result);


// render(<Result>{
//     data: [
//         {id: 1, name: 'A', sex: 'male'},
//         {id: 2, name: 'B'}
//     ]
// })

// render({
//     data: [
//         { id: 1, name: 'A', sex: 'male' },
//         { id: 2, name: 'B' }
//     ]
// } as Result)

// 上面两个 render 等价,因为 <Result> 在 React 中有歧义,故不推荐使用

interface StringArray {
    [index: number]: string
}
let chars: StringArray = ['A', 'B']

interface Names1 {
    [x: string]: string;
    // [z: number]: number; // Error 问题一
    [z: number]: string;
}

interface Names2 {
    [x: string]: any;
    [z: number]: number
}
let names2: Names2 = {
    '1': 123,
    // 2: '456' // Error 问题二
    2: 456
}

/**
 * 上面两个问题
 * 
 * 问题一 Error 的原因是:属性的类型声明 string 和 number 存在隐式转换问题,声明时,number 值类型必须是 string 值类型的子集。
 * 
 * 问题二 Error 的原因是:若 string 和 number 互相转换成功,则需要同时满足两者的类型声明。
 * 
 */

函数类型接口

// let add1: (x: number, y: number) => number;

// interface Add {
//     (x: number, y: number): number
// }

type Add = (x: number, y: number) => number

let add1: Add = (a, b) => a + b

// 混合接口

interface Lib {
    (): void;
    version: string;
    doSomething(): void;
}

function getLib() {
    let lib: Lib = (() => { }) as Lib;
    lib.version = '1.0';
    lib.doSomething = () => { }
    return lib;
}

let lib1 = getLib();
lib1();
lib1.doSomething();

let lib2 = getLib();

函数定义

// 函数定义
function add11(x: number, y: number) {
    return x + y;
}

let add22: (x: number, y: number) => number;

type add33 = (x: number, y: number) => number;

interface add44 {
    (x: number, y: number): number
}

// add1(1, 2, 3)

function add55(x: number, y?: number) {
    return y ? x + y : x;
}
add55(1)

function add66(x: number, y = 0, z: number, q = 1) {
    return x + y + z + q;
}
console.log(add66(1, undefined, 3));

function add77(x: number, ...rest: number[]) {
    return x + rest.reduce((pre, cur) => pre + cur)
}
console.log(add77(1, 2, 3, 4, 5))

function add88(...rest: number[]): number;
function add88(...rest: string[]): string;
function add88(...rest: any[]): any {
    let first = rest[0];
    if (typeof first === 'string') {
        return rest.join('')
    }
    if (typeof first === 'number') {
        return rest.reduce((pre, cur) => pre + cur)
    }
}
console.log(add88(1, 2, 3));
console.log(add88('1', '2', '3'));

类的实现 类的继承 类的成员修饰符

class Dog {
    constructor(name: string) {
        this.name = name;
    }
    name: string;
    run() {

    }
    private pri() { }
    protected pro() { }
    readonly legs: number = 4;
    static food: string = 'bones';
}
console.log(Dog.prototype);
let dog = new Dog('wangwang');
console.log(dog);
// dog.pri()
// dog.pro()
console.log(Dog.food);
// console.log(dog.food);

class Husky extends Dog {
    constructor(name: string, public color: string) {
        super(name);
        // this.color = color;
        // this.pri();
        this.pro();
    }
    // color: string;
}

let husky = new Husky('husky', 'red');
// console.log(husky.food);
console.log(Husky.food);

抽象类 多态

abstract class Animal {
    eat() {
        console.log('eat');
    }
    abstract sleep(): void;
}
// let animal = new Animal();

class Dog1 extends Animal {
    constructor(name: string) {
        super();
        this.name = name;
    }
    name: string;
    run() { }
    sleep() {
        console.log('dog sleep');
    }
}
let dog1 = new Dog1('wangwang');
dog1.eat();

class Cat extends Animal {
    sleep() {
        console.log('Cat sleep')
    }
}
let cat = new Cat();

let animals: Animal[] = [dog1, cat];
animals.forEach(i => {
    i.sleep();
})

class WorkFlow {
    step1() {
        return this;
    }
    step2() {
        return this;
    }
}
new WorkFlow().step1().step2();

class Myflow extends WorkFlow {
    next() {
        return this;
    }
}
new Myflow().next().step1().next().step2();

类与接口

image.png

/* 
    1、类实现接口必须实现接口中声明的所有的属性
    2、接口只能约定类的公有成员
    3、接口不能约束类的构造函数
*/

interface Human {
    // new(name: string): void
    name: string;
    eat(): void;
}
class Asian implements Human {
    constructor(name: string) {
        this.name = name;
    }
    name: string
    eat() { }
    sleep() { }
}

/*  
    接口继承    
*/

interface Man extends Human {
    run(): void
}
interface Child {
    cry(): void
}
interface Boy extends Man, Child { }
let boy: Boy = {
    name: '',
    run() { },
    eat() { },
    cry() { }
}

class Auto {
    state = 1
    private state2 = 0
}
interface AutoInterface extends Auto {
}
// class C implements AutoInterface {
//     state = 1
// }
class C extends Auto implements AutoInterface {
    state = 1
}
class Bus extends Auto implements AutoInterface {
}

泛型 泛型函数与泛型接口

泛型:不预先确定的数据类型,具体的类型在使用的时候才能确定。

// 方式一
function log<T>(value: T): T {
    console.log(value);
    return value;
}
log<string[]>(['a', 'b']);
log(['a', 'b'])

// 方式二
// type Log = <T>(value: T) => T
// let myLog: Log = log

// 方式三
// interface Log {
//     <T>(value: T): T
// }
// interface Log<T> {
//     (value: T): T
// }
interface Log<T = string> {
    (value: T): T
}
let myLog: Log<number> = log
myLog(1)
let myLog2: Log = log
myLog2('1')
class Log<T> {
    // static a: T = 1; // 静态成员不能使用泛型
    run(value: T) {
        console.log(value);
        return value;
    }
}
let log1 = new Log<number>();
log1.run(1)
let log2 = new Log()
log2.run('1')

interface Length {
    length: number
}
function log<T extends Length>(value: T): T {
    console.log(value, value.length);
    return value;
}
log(1);
log('123')
log({ length: 1 })

泛型的好处:

  1. 函数和类可以轻松地支持多种类型,增强程序的扩展性
  2. 不必写多条函数重载,冗长的联合类型声明,增强代码可读性
  3. 灵活控制类型之间的约束

类型检查机制

TypeScript 编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。

作用:辅助开发,提高开发效率。

类型推断

不需要指定变量的类型(函数的返回值类型),TypeScript 可以根据某些规则自动地为其推断出一个类型。基础类型推断、最佳通用类型推断、上下文类型推断

// 从右往左推断
let a = 1;
let b = [1, null]

let c = (x = 1) => x + 1

// 从左往右推断
window.onkeydown = (event) => {
    // console.log(event.button);
}


interface Foo {
    bar: number
}
let foo = {} as Foo;
foo.bar = 1;

let foo2: Foo = {
    bar: 1
}

类型兼容性

export { }
/**
 * 当一个类型 Y 可以被赋值给另一个类型 X 时,我们就可以说类型 X 兼容类型 Y
 * X 兼容 Y:X(目标类型)= Y(源类型)
 * 口诀:
 * 结构之间兼容:成员少的兼容成员多的
 * 函数之间兼容:参数多的兼容参数少的
 */

let s: string = 'a';
s = null;  // 这里 s 会报错,需要在 tsconfig.json 中将 "strictNullChecks" 设置为 false

// 接口兼容性
interface X {
    a: any;
    b: any;
}
interface Y {
    a: any;
    b: any;
    c: any;
}
let x: X = {
    a: 1,
    b: 2
}
let y: Y = {
    a: 1,
    b: 2,
    c: 3
}
x = y;
// y = x; // 报错
// 这里 y 可以赋值给 x ,x 不能赋值给 y,因为 y 的属性包含了 x 的属性,所有能兼容 x

// 函数兼容性
type Handler = (a: number, b: number) => void
function hof(handler: Handler) {
    return handler
}
// 1) 参数个数
let handler1 = (a: number) => { }
hof(handler1);
let handler2 = (a: number, b: number, c: number) => { }
// hof(handler2) // 这里 handler2 不兼容,原因是 handler2 参数数量大于 Handler 参数数量,就比如 Handler 只能处理两个参数,你传进来一个参数或者两个参数我都能处理,你传进来三个超出了我的能力范围,就只能报错了

// 2) 可选参数和剩余参数
let a = (p1: number, p2: number) => { }
let b = (p1?: number, p2?: number) => { }
let c = (...args: number[]) => { }
a = b;
a = c;
// b = c;
// b = a;
// b = c 和 b = a 这里会报错,报错的原因是:b 只能接收两个可选参数,a 有两个固定参数,所以 b 有可能处理不了,就报错了。 c 有扩展参数,参数个数不定,所以 b 也没能力处理。

interface Point3D {
    x: number;
    y: number;
    z: number;
}
interface Point2D {
    x: number;
    y: number;
}
let p3d = (point: Point3D) => { };
let p2d = (point: Point2D) => { };
p3d = p2d;
// p2d = p3d; // 报错,报错原因:这里的 interface 跟上面的 interface 是反的,上面的是属性少的兼容属性多的,这里是属性多的兼容属性少的,这里可以把 interface 中的属性当作参数来看,参数少的兼容参数多的。 p3d 参数多,所以 p2d 不兼容 p3d 导致报错。如果要 p2d 兼容 p3d 也是可以做到的,需要在 tsconfig.json 中设置 "strictFunctionTypes" 为 false

// 3) 返回值类型
// ts 要求函数的返回值类型必须相同或者为其子类型。
let f = () => ({ name: 'Alice' });
let g = () => ({ name: 'Alice', location: 'Beijing' })
f = g
// g = f // 报错,原因: g 要求对象有两个属性,但是 f 返回值对象只有一个属性

// 函数重载
// 1) 函数重载的列表
function overload(a: number, b: number): number; // 函数定义 // 目标函数
function overload(a: string, b: string): string; // 函数定义 // 目标函数
// 2) 函数的具体实现
function overload(a: any, b: any): any { }; // 函数实现 // 源函数
// function overload(a: any): any { }; // 函数实现 // 源函数
// 程序在运行的时候,编译器会查找重载列表,然后使用第一个匹配的定义来执行相应函数。所以,在重载列表中,目标函数的参数要多于源函数的参数,而且返回值类型也要符合相应要求。

// 枚举类型兼容性
enum Fruit { Apple, Banana }
enum Color { Red, Yellow }
// 枚举类型和数值类型是互相兼容的
let fruit: Fruit.Apple = 3;
let no: number = Fruit.Apple
// 枚举之间是完全不兼容的
// let color: Color.Red = Fruit.Apple // 报错

// 类兼容性
// 类的兼容性和接口比较相似,只比较结构。
// 在比较两个类是否兼容的时候,静态成员和构造函数是不参与比较的。
// 如果两个类具有相同的实例成员,那么他们的实例就可以相互兼容。
class A {
    constructor(p: number, q: number) { }
    id: number = 1
    // private name: string = ''
}
class B {
    static s = 1
    constructor(p: number) { }
    id: number = 2
    // private name: string = ''
}
let aa = new A(1, 2);
let bb = new B(1);
aa = bb;
bb = aa;
// 如果类中含有私有成员,只有父类和子类可以相互兼容。 
class C extends A { }
let cc = new C(1, 2);
aa = cc;
cc = aa;

// 泛型兼容性
interface Empty<T> {

}
let obj1: Empty<number> = {};
let obj2: Empty<string> = {};
// obj1 = obj2; // 泛型接口中没有任何成员的时候,这两个变量是互相兼容的。只有类型参数 T 在接口中使用的时候,才会影响泛型的兼容性。

// 泛型函数兼容性
let log1 = <T>(x: T): T => {
    console.log('x');
    return x;
}
let log2 = <U>(y: U): U => {
    console.log('y');
    return y;
}
log1 = log2;
log2 = log1;
// 如果两个泛型函数的定义相同,但是没有指定类型参数。那么他们之间也是可以相互兼容的。

类型保护

类型保护

TypeScript 能够在特定的区块中保证变量属于某种确定的类型.

可以在此区块中放心地引用此类型的属性,或者调用此类型的方法。

/**
 * 类型保护
 * TypeScript 能够在特定的区块中保证变量属于某种确定的类型。
 * 可以在此区块中放心地引用此类型的属性,或者调用此类型的方法。
 */
export { }

enum Type { Strong, Week }

class Java {
    helloJava() {
        console.log('Hello Java')
    }
    java: any
}
class JavaScript {
    helloJavaScript() {
        console.log('Hello JavaScript')
    }
    javascript: any
}

// 方案五 通过创建类型保护函数 来判断对象类型
function isJava(lang: Java | JavaScript): lang is Java {
    return (lang as Java).helloJava !== undefined;
}


// function getLanguage(type: Type, x: string | number) { // 结合方案四使用
function getLanguage(type: Type) {
    let lang = type === Type.Strong ? new Java() : new JavaScript();

    // if (lang.helloJava) {  // 报错
    //     lang.helloJava()
    // } else {
    //     lang.helloJavaScript()
    // }

    // // 方案一 as
    // if ((lang as Java).helloJava) {
    //     (lang as Java).helloJava()
    // } else {
    //     (lang as JavaScript).helloJavaScript()
    // }

    // // 方案二 instanceof
    // if (lang instanceof Java) {
    //     lang.helloJava()
    // } else {
    //     lang.helloJavaScript()
    // }

    // // 方案三 in // 用 in 来判断属性是不是属于某个对象
    // if ('java' in lang) {
    //     lang.helloJava();
    // } else {
    //     lang.helloJavaScript()
    // }

    // // 方案四 typeof 只能判断基础类型
    // if (typeof x === 'string') {
    //     x.toString()
    // } else {
    //     x.toFixed(2)
    // }

    // 方案五
    if (isJava(lang)) { // 因为上面 isJava 返回类型是 lang is Java,所以作用域内可以使用 helloJava
        lang.helloJava()
    } else {
        lang.helloJavaScript()
    }

    return lang
}

getLanguage(Type.Strong);

高级类型

所谓高级类型就是 TypeScript 为了保障语言的灵活性所引用的一些语言特性,这些特性将有助于我们应对复杂多变的开发场景。

交叉联合与联合类型

交叉类型

交叉类型是指将多个类型合并为一个类型,新的类型将具有所有类型的特性,所以交叉类型特别适合对象混用的场景。

交叉类型 '&' 符号连接,交叉类型听起来像是取类型的交集,但实际上是取类型的并集。

// 交叉类型
// 交叉类型是指将多个类型合并为一个类型,新的类型将具有所有类型的特性,所以交叉类型特别适合对象混用的场景。
// 交叉类型 '&' 符号连接,交叉类型听起来像是取类型的交集,但实际上是取类型的并集。

interface DogInterface {
    run(): void
}
interface CatInterface {
    jump(): void
}
let pet: DogInterface & CatInterface = {
    run() { },
    jump() { }
}

联合类型

所谓联合类型是指,声明的类型并不确定,可以为多个类型中的一个。

联合类型符号 '|',联合类型给人的感觉是可以取所有成员的并集,但真实情况是只能取所有成员的交集。

// 联合类型
// 所谓联合类型是指,声明的类型并不确定,可以为多个类型中的一个。
// 联合类型符号 '|'

let a: number | string = 'a'
// 顺便介绍 字面量类型,不仅限定变量的类型,还限定变量取值在一定范围内
// 字符串字面量联合类型
let b: 'a' | 'b' | 'c' = 'b'
// 数值字面量联合类型
let c: 1 | 2 | 3 = 2

// 对象的联合类型
class Dog implements DogInterface {
    run() { }
    eat() { }
}
class Cat implements CatInterface {
    jump() { }
    eat() { }
}
enum Master { Boy, Girl }
function getPet(master: Master) {
    let pet = master === Master.Boy ? new Dog() : new Cat();
    pet.eat(); // 在类型没有确定的情况下,只能访问所有类型的共有成员。联合类型给人的感觉是可以取所有成员的并集,但真实情况是只能取所有成员的交集。
    // pet.run(); 
    return pet;
}

// 可区分的联合类型,实际是结合了联合类型和字面量类型的一种类型保护方法。
// 他的核心思想是一个类型如果是多个类型的联合类型,并且每个类型之间有一个类型的公共属性,那么我们可以凭借这个公共属性创建类型保护区块。
interface Square {
    kind: 'square';
    size: number;
}
interface Rectangle {
    kind: 'rectangle';
    width: number;
    height: number;
}
interface Circle {
    kind: 'circle';
    r: number;
}
type Shape = Square | Rectangle | Circle; // Circle 为新加
function area(s: Shape) {
    switch (s.kind) {
        case 'square':
            return s.size * s.size;
        case 'rectangle':
            return s.height * s.width;
    }
}

console.log(area({ kind: 'circle', r: 1 })); // Shape 类型扩展 Circle 后,switch case 遗漏,TypeScript 没有报错,area 返回值为 undefined , 如何用 TypeScript 约束这种模式,给出相应的错误提示呢(目的是有 case 遗漏的时候 TypeScript 能给出错误提示)?
// // 方案一 使用函数返回类型约束
// function area1(s: Shape): number { // 这里报错是对的,TypeScript 检查到 case 有遗漏。Shape 添加 Cirice 类型后,number 这里报错:Function lacks ending return statement and return type does not include 'undefined'.
//     switch (s.kind) {
//         case 'square':
//             return s.size * s.size;
//         case 'rectangle':
//             return s.height * s.width;
//     }
// }

// // 方案二 利用 never 类型
// function area2(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.r ** 2;
//         default:
//             return ((e: never) => { throw new Error(e) })(s); // 这里报错是正确的,说明 TypeScript 检查到 case 有遗漏。这里函数的作用是检查 s 是不是 never 类型,如果 s 是 never 类型,说明前面分支都被覆盖了,这个分支永远不会走到;如果 s 不是 never 类型,说明前面分支有遗漏。Argument of type 'Circle' is not assignable to parameter of type 'never'.
//     }
// }

索引类型

从对象中获取属性的值,然后建立一个集合。

export { }
// 索引类型
// 从对象中获取属性的值,然后建立一个集合。
let obj = {
    a: 1,
    b: 2,
    c: 3
}
function getValues(obj: any, keys: string[]) {
    return keys.map(key => obj[key])
}
console.log(getValues(obj, ['a', 'b'])); // [1, 2]
console.log(getValues(obj, ['e', 'f'])); // [undefined, undefined] // 返回 undefined ,但是 TypeScript 并没有报错。TypeScript 如何对这种模式进行约束呢,这里就用到了索引类型。

// 了解索引类型前,先了解一些概念
// 1、keyof T ,含义是表示类型 T 的所有公共属性的字面量的联合类型。例子:
// keyof T
interface Obj {
    a: number,
    b: string
}
let key: keyof Obj; // let key: 'a' | 'b'; key 的类型变成了 'a' 和 'b' 的字面量联合类型。key = 'a'; key = 'b';

// 2、T[K] 含义表示对象 T 的属性 K 所代表的类型。 例子:
// T[K]
let value: Obj['a']; // let value: number; value 的类型变成了 Obj['a'] 所指定的类型。 value = 1; value = 2;

// 3、泛型约束 
// T extends U  表示泛型变量可以通过继承某个类型获得某些属性

// 了解上面三个概念后,我们来改造 getValues 函数。

function getValues1<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
    return keys.map(key => obj[key])
}
console.log(getValues1(obj, ['a', 'b'])); // [1, 2]
console.log(getValues1(obj, ['e', 'f'])); // 这里 e f 会报错,这是正确的,指定不在 obj 里面的属性的话,TypeScript 会报错。
/* 
解析

第一步 泛型 T 约束 obj 参数类型。
function getValues1<T>(obj: T, keys) {
    return keys.map(key => obj[key])
}
第二步 泛型 K 约束 keys 参数类型。
function getValues1<T, K>(obj: T, keys: K[]) {
    return keys.map(key => obj[key])
}
第三步 K extends keyof T 进一步约束 keys 参数类型,keyof T 在这里相当于 'a' | 'b' | 'c',K extends keyof T 说明 K 上面只存在 a b c 属性。
function getValues1<T, K extends keyof T>(obj: T, keys: K[]) {
    return keys.map(key => obj[key])
}
第四步 T[k][] 约束了函数返回类型,是个 T[K] 类型数组,T[K] 在这里相当于 number,因为 T 是 obj, T['a']、T['b']、T['c'] 都是 number,所以 T[K][] 在这里是 number 类型数组。如果 T['b'] 是个 string 值的话(比如:obj.b = '2'),那这里 T[K][] 代表的是 number string 交叉类型数组。
function getValues1<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
    return keys.map(key => obj[key])
}

console.log(getValues1(obj, ['a', 'b'])); // [1, 2]
console.log(getValues1(obj, ['e', 'f']));
这里 e f 会报错,这是正确的,指定不在 obj 里面的属性的话,TypeScript 会报错。
由此可见索引类型可以实现对对象属性的查询和访问,然后在配合泛型约束是我们能够建立对象、对象属性、对象属性值之间的约束关系。

*/

映射类型

通过映射类型,我们可以从一个旧的类型生成一个新的类型。

比如把一个类型中的所有属性变为只读。

interface Obj {
    a: string;
    b: number;
    c: boolean;
}
// 同态类型,只会作用于 Obj 属性,不会引入新的属性。
// 将所有属性变为只读
type ReadonlyObj = Readonly<Obj>;
// type Readonly<T> = {
//     readonly [P in keyof T]: T[P];
// };
// type ReadonlyObj = {
//     readonly a: string;
//     readonly b: number;
//     readonly c: boolean;
// }

// 将所有属性变为可选
type PartialObj = Partial<Obj>;
// type Partial<T> = {
//     [P in keyof T]?: T[P];
// };
// type PartialObj = {
//     a?: string | undefined;
//     b?: number | undefined;
//     c?: boolean | undefined;
// }

// 选择部分属性生成新的类型 
type PickObj = Pick<Obj, 'a' | 'b'>
// type Pick<T, K extends keyof T> = {
//     [P in K]: T[P];
// };
// type PickObj = {
//     a: string;
//     b: number;
// }

// 非同态类型,会创建新的属性,第一个参数是新的属性,第二个参数是一个已知的类型
type RecordObj = Record<'x' | 'y', Obj>
// type Record<K extends keyof any, T> = {
//     [P in K]: T;
// };
// type RecordObj = {
//     x: Obj;
//     y: Obj;
// }

条件类型

条件类型是一种由条件表达式所决定的类型。

T extends U ? X : Y ,意思是如果类型 T 可以被赋值为类型 U,那么结果类型就是 X 类型,否则是 Y 类型。

条件类型是类型具有了不唯一性,同样增加了语言的灵活性。

// 条件类型,条件类型嵌套
// T extends U ? X : Y
type TypeName<T> =
    T extends string ? 'string' :
    T extends number ? 'number' :
    T extends boolean ? 'boolean' :
    T extends undefined ? 'undefined' :
    T extends Function ? 'function' :
    'object';

type T1 = TypeName<string>; // T1: string
type T2 = TypeName<string[]>; // T2: object

// 分布式条件类型
// (A | B) extends U ? X : Y 
// 拆解后:(A extends U ? X : Y) | (B extends U ? X : Y)
type T3 = TypeName<string | string[]> // T3: "string" | "object"


type Diff<T, U> = T extends U ? never : T;

type T4 = Diff<'a' | 'b' | 'c', 'a' | 'e'>
// 拆解
// Diff<'a', 'a' | 'e'> | Diff<'b', 'a' | 'e'> | Diff<'c', 'a' | 'e'>
// 结果: never | 'b' | 'c' // never 表示永远不存在的值的类型
// 结果:'b' | 'c'

type NotNull<T> = Diff<T, undefined | null>; // 过滤 undefined 和 null
type T5 = NotNull<string | number | undefined | null>; // T5: string | number

// Diff 和 NotNull 官方已经帮我们实现了,官方对应的是 Exclude<T, U> 和 NonNullable<T>,实际使用的时候直接调用就行。
// 官方还预定了一些条件类型,Extract<T, U> 与 Exclude 相反,Exclude 是过滤掉可以赋值给 U 的类型,Extract 是抽取出可以赋值给 U 的类型。
type T6 = Extract<'a' | 'b' | 'c', 'a' | 'e'>; // T6: 'a'

// ReturnType<T> 可以获取一个函数返回值的类型
type T7 = ReturnType<() => string>
// type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any; // infer 待推断、延迟推断,需要根据实际情况来确定

ES6 与 CommonJS 的模块系统

src/es6/a.ts

// 单独导出
export let a = 1;

// 批量导出
let b = 2;
let c = 3;
export { b, c }

// 导出接口
export interface P {
    x: number;
    y: number;
}

// 导出函数
export function f() { }

// 导出时起别名
function g() { }
export { g as G }

// 默认导出,无需函数名
export default function () {
    console.log("i'm default")
}

// 引入外部模块,重新导出
export { str as hello } from './b';

src/es6/b.ts

let str = 1
export {
    str
}

src/es6/c.ts

import { a, b, c } from './a'; // 批量导入
import { P } from './a'; // 导入接口
import { f as F } from './a'; // 导入时起别名
import * as All from './a'; // 导入模块中所有成员,绑定在 all 上
import myFunction from './a'; // 不加 {},导入默认

console.log(a, b, c);

let p: P = {
    x: 1,
    y: 1
}

console.log(All);

myFunction();

src/es6/d.ts

export = function () { // TypeScript 特殊语法,兼容语法。 这个语法会被编译成 module.exports,相当于 commonJS 中的顶级导出了。同时意味着这个文件中不能有其他导出了,如果想导出其他数据,需要合并到一个对象中进行导出。
    console.log("i'm default")
}

// export let a = 1

src/node/a.node.ts

export { }

let a = {
    x: 1,
    y: 1
}

// 整体导出
module.exports = a;

src/node/b.node.ts

// exports === module.exports
// 导出多个变量

// module.exports = {}  会覆盖 exports.c exports.d 的导出

exports.c = 3
exports.d = 4

src/node/c.node.ts

let c1 = require('./a.node')
let c2 = require('./b.node')
let c3 = require('../es6/a')

console.log(c1)
console.log(c2)

// es6 与 commonjs 混用,不建议混用,很容易发生错误
// c3()  // 报错
console.log(c3)
c3.default() // 不报错,需要调用 default 方法,反直觉,容易发生错误

// 如何处理两个模块间的不兼容性问题呢?两个方案:一是两个模块系统不要混用;二是使用 TypeScript 的兼容性语法。详见 d.ts
import c4 = require('../es6/d'); // TypeScript 特殊语法,兼容语法。 如果 "esModuleInterop": true, 也可以直接使用 es6 语法导入:import c4 from '../es6/d'; 
c4();
// c3 和 c4 对比,c3 es6 和 commonJS 混用时,需要调用 default 方法才行。c4 es6 和 commonJS 混用时,使用了 TypeScript 特殊兼容语法,可以直接调用 c4(),不需要调用 default 函数。

使用命名空间

src/a.ts

// 命名空间可以有效的避免全局污染。使用全局类库的时候,命名空间仍然是一个比较好的解决方案。
namespace Shape { // 在命名空间内可以定义任意多的变量,这些变量只能在 Shape 命名空间下可见。
    const pi = Math.PI;
    export function cricle(r: number) { // 如果想让成员对全局可见的话,可以使用 export 关键字导出。
        return pi * r ** 2
    }
    // export function square(x: number) { // 在命名空间中,导出的成员是不可以重复定义的,接口中是可以重复定义的。
    //     return x * x
    // }
}

// 命名空间也可以进行拆分,可见 b.ts 文件。也存在 Shape 命名空间,他们共享一个命名空间。

src/b.ts

/// <reference path='a.ts' /> 

namespace Shape {
    export function square(x: number) {
        return x * x
    }
}

// 命名空间的调用方法。
Shape.cricle(1); // 报错,cricle 是 a.ts 中的,这里需要对 a.ts 进行引用。引用语法:/// <reference path='a.ts' />
Shape.square(1);

// 明确一个原则: 命名空间和模块不要混用,不要在一个模块中使用命名空间。命名空间最好在一个全局的环境中使用。



// 命名空间成员别名
import cricle = Shape.cricle; // 这里的 import 和模块的 import 没有任何关系。
cricle(1);

// 在 TypeScript 的早期版本中,命名空间也叫内部模块,本质上就是一个闭包,可以用于隔离作用域。
// 随着 ES6 模块的引入,内部模块这个名称现在已经不在叫了,TypeScript 保留的命名空间,更多是考虑对全局变量时代的一种兼容。
// 现在在一个完全的模块化系统中,我们可以不必使用命名空间。

理解声明合并

所谓声明合并就是程序会把多个地方的相同声明合并为一个声明。 这样做有一个好处,就是把程序中散落在各个地方的重名声明合并在一起, 比如你在程序中在多个地方定义了同样名字的接口,那么你在使用接口的时候就会对这个多出的定义具有感知,通过声明合并就会避免对接口成员的遗漏。

src/merge.ts

export { }
// 声明合并
// 所谓声明合并就是程序会把多个地方的相同声明合并为一个声明。
// 这样做有一个好处,就是把程序中散落在各个地方的重名声明合并在一起,
// 比如你在程序中在多个地方定义了同样名字的接口,那么你在使用接口的时候就会对这个多出的定义具有感知,通过声明合并就会避免对接口成员的遗漏。

// 接口的声明合并
interface A {
    x: number
    // y: string  // 报错,对于接口中的非函数成员,要保证唯一性,如果不唯一的话类型必须相同。 y: number
    foo(bar: number): number // 5 // 对于函数成员,每一个函数都会被声明为一个函数重载
    foo(bar: 'a'): number // 2
}
interface A {
    y: number
    foo(bar: string): string // 3
    foo(bar: number[]): number[] // 4
    foo(bar: 'b'): number // 1
}

let a: A = {
    x: 1,
    y: 1,
    foo(bar: any) { // 函数重载的时候需要注意函数声明的顺序,因为编译器会按顺序进行匹配,顺序原则:接口内部按照书写顺序,接口之间的话后面的接口会排在前面。也有一个例外,如果函数的参数是字符串字面量,那么这个声明就会被提升到函数的最顶端。
        return bar
    }
}

// 命名空间和函数的合并
function Lib() { }
namespace Lib {
    export let version = '1.0' // export 相当于给 Lib 函数增加了一个属性,JavaScript 中给函数增加属性是很常见的模式。
}

console.log(Lib)

// 命名空间和类的合并
class C { }
namespace C {
    export let state = 1; // 相当于给类添加了一个静态类型属性
}

// 命名空间和枚举进行合并
enum Color {
    Red, Yellow, Blue
}
namespace Color {
    export function mix() { } // 相当于给枚举对象添加了一个静态方法
}
// 命名空间与函数和类进行合并时,必须要放在后面,否则 TypeScript 会报错。与枚举类型合并不受限制。
// 在我们的程序中,如果有多处同名的声明,这并不是一个好的模式,最好是把他们封装在一个模块内,TypeScript 具有这种特性是为了照顾旧的开发模式,这使我们在工程中如果引入 TypeScript 可以和老的代码共存。

如何编写声明文件

如何在 TypeScript 中引入外部类库,如何为他们编写声明文件。

类库一般分为三类,全局类库,模块类库,UMD 类库。

src/libs/index.ts

export { }
// 如何编写声明文件
// 如何在 TypeScript 中引入外部类库,如何为他们编写声明文件。
// 类库一般分为三类,全局类库,模块类库,UMD 类库。
// JQuery 是 UMD 类库。


// 如果社区 @types 有生命文件,可以直接安装使用。有时候类库的声明文件包含在源码中,不需要安装。
// npm install -S jquery
// npm install -D @types/jquery
import $ from 'jquery';
$('.app').css('color', 'red');


// 如何编写声明文件
// 全局类库
globalLib({ x: 1 }) // 报错无法找到声明文件,创建 global-lib.d.ts 后恢复。
globalLib.doSomething();

// 模块类库
import moduleLib from './module-lib'; // 报错,无法找到声明文件。添加 module-lib.d.ts 后恢复。
moduleLib.doSomething();

// UMD 库
import umdLib from './umd-lib'; // 报错,无法找到声明文件。添加 umd-lib.d.ts 后恢复。
umdLib.doSomething(); // umd 库可以和 globalLib 库一样使用全局引用,TypeScript 会报错,不建议使用全局引用,设置 "allowUmdGlobalAccess": true, 可以关闭报错。




// 插件
// 模块插件
// npm install -S moment
import m from 'moment';
m.myFunction = () => { // 报错,类型“typeof moment”上不存在属性“myFunction”。ts(2339);我们可以使用 declare module 处理这个问题,这样我们可以给一个外部类库增加自定义方法。
}
declare module 'moment' {
    export function myFunction(): void
}
// 全局插件,这会对全局命名空间造成污染,一般不建议这么做。
declare global {
    namespace globalLib {
        function doAnything(): void
    }
}
globalLib.doAnything = () => { }

// 声明文件的依赖
// 查看 JQuery 声明文件中的依赖。
/**
        /// <reference types="sizzle" /> // 模块依赖,在 @types 目录下
        /// <reference path="JQueryStatic.d.ts" /> // 路径依赖,同级路径下
        /// <reference path="JQuery.d.ts" />
        /// <reference path="misc.d.ts" />
        /// <reference path="legacy.d.ts" />

        export = jQuery;
 */

src/libs/global-lib.js

// 全局库
function globalLib(options) {
    console.log(options);
}

globalLib.version = '1.0.0';

globalLib.doSomthing = function () {
    console.log('globalLib do something');
}

src/libs/global-lib.d.ts

// 声明文件

// declare 关键字,可以为外部变量提供一些声明。
declare function globalLib(options: globalLib.Options): void;

declare namespace globalLib {
    const version: string;
    function doSomething(): void;
    interface Options { // 这个接口可以放在全局,放在全局就对外暴漏了,如果不想对外暴漏就放在命名空间内。
        [key: string]: any
    }
}

src/libs/module-lib.js

// 模块类库
const version = '1.0.0';

function doSomething() {
    console.log('moduleLib do something');
}

function moduleLib(options) {
    console.log(options);
}

moduleLib.version = version;
moduleLib.doSomething = doSomething;

module.exports = moduleLib;

src/libs/module-lib.d.ts

// 模块声明文件
declare function moduleLib(options: Options): void

interface Options { // 这里放在外面不会向外暴漏,原因是因为这个声明文件是个模块,所以这个接口就不会向外暴漏。
    [key: string]: any
}

declare namespace moduleLib {
    const version: string
    function doSomething(): void
}

export = moduleLib; // TypeScript 特殊语法,兼容语法。 这个语法会被编译成 module.exports,相当于 commonJS 中的顶级导出了。同时意味着这个文件中不能有其他导出了,如果想导出其他数据,需要合并到一个对象中进行导出。

src/libs/umd-lib.js

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define(factory);
    } else if (typeof module === 'object' && module.exports) {
        module.exports = factory();
    } else {
        root.umdLib = factory();
    }
}(this, function () {
    return {
        version: '1.0.0',
        doSomething() {
            console.log('umdLib do something')
        }
    }
}));

src/libs/umd-lib.d.ts

declare namespace umdLib {
    const version: string
    function doSomething(): void
}

// 专为 umd 库设置的语句。如果我们编写 umd 库,这个语句是不可缺少的。
export as namespace umdLib

export = umdLib

配置 tsconfig

编译工具:从 ts-loader 到 Bable

代码检查工具:从 TSLint 到 ESLint

使用 Jest 进行单元测试

创建项目

组件与类型

函数组件与类组件

高阶组件与 Hooks

事件处理与数据请求

列表渲染与路由

Redux 与类型

服务端环境搭建

列表的 CRUD

导出 Excel

搭建 Vue 开发环境

组件封装

组件发布

共存策略

宽松策略

严格策略