typescript 学习笔记

基本类型

/**
 * Boolean
 */
const bol1: boolean = true;

/**
 * String
 */
const str2: string = `bol: ${true}`;

/**
 * Number
 */
const num1: number = 123;

/**
 * Array
 */
const arr0: (number|string)[] = [1, '2'];
const arr2: Array<string|boolean|number> = ['a', 'b', false, 5];

/**
 * Object
 */
const obj: object = { a: 1, b: 'a' };

/**
 * Tuple(元组)
 *
 * 已知元素数量和类型且可重复的 数组
 */
const tpl: [string, number, boolean, boolean] = ['a', 2, true, true];

/**
 * Enum(枚举)
 *
 * 为一组 数值或字符串 指定别名(方便记忆)
 */
enum Color {
    Red,
    Green,
    Blue = 4,
    Pink = 'pink',
};
// 枚举属性是只读的,不支持修改
// 访问枚举值 类似访问对象属性
// 变量的类型限定为枚举时,变量的值只能是枚举成员之一
const r: Color = Color.Red;   // r === 0
const g: Color = Color.Green; // g === 1
const b: Color = Color.Blue;  // b === 4
const p: Color = Color.Pink;  // p === "pink"
// 与对象的区别
enum EDirection {
    Up,
    Down,
    Left,
    Right,
}
const ODirection = {
    Up: 0,
    Down: 1,
    Left: 2,
    Right: 3,
} as const;

// 枚举能直接应用类型
function run1(dir: EDirection): void {};

run1(EDirection.Up);

// 对象(字面量)不具有类型,需要先申明类型再应用
// (keyof typeof ODirection) 表示限制对象属性为 "Up" | "Down" | "Left" | "Right"
// 枚举则默认存在这种机制
type Direction = typeof ODirection[keyof typeof ODirection];

function run2(dir: Direction): void {};

run2(ODirection.Up);

/**
 * Any
 *
 * 任意类型,会跳过类型检查
 * 不指定类型默认为 any
 */
let anyVar: any = 123;
const anyArr: any[] = [ true, 123, 'aaa' ];

/**
 * Void
 *
 * 无任何类型,与 any 相反
 */
function voidFn(): void {}
let void1: void = undefined;

/**
 * Undefined
 */
let u: undefined = undefined;

/**
 * Null
 */
let n: null = null;

/**
 * Unknown
 *
 * 未知类型,相当于 any,但是不允许执行变量的方法(更安全)
 */
let uk: unknown = 'abc';
// uk.toString(); // Error

/**
 * Never
 *
 * 永不存在的值 的类型
 */
// 例如抛出异常的函数,不能具有可访问的终结点
// 因为抛出异常会终止函数执行,也就无法到达终点返回值了
function err(): never {
    throw new Error('message');
}

接口

/**
 * 属性类型接口
 */
// 限制某个对象的属性类型(这里是入参)
function fn1(obj: { str: string }): string {
    return obj.str;
}

fn1({ str: "abc" });

// 使用接口实现
interface IObj {
    str: string;

    // 不能包含具体实现
    // str2: string = 'abc'; // Error

    // 属性可以是个复杂对象
    aa?: {
        ba: {
            ca: string;
        },
        bb: [ number, boolean ];
    };

    /**
     * 可选属性
     */
    num?: number;

    /**
     * 只读属性,类似于 const 变量
     */
    readonly bol?: boolean;

    /**
     * 索引签名 额外的属性
     * 'string', 'number', 'symbol'.
     */
    [propName: string]: any;
}

// 应用接口类似于申明类型
function fn2(obj: IObj) {
    return obj.str;
}

fn2({
    str: "abc",
    num: 123,
    aa: {
        ba: { ca: "a" },
        bb: [1, true]
    },
});

// 属性类型校验,应为 string
// fn2({ str: 123 }); // Error

// 属性存在性校验,必须存在 str,不存在 abc
// fn2({ abc: 'abc' }); // Error

const obj1: IObj = { str: "abc", bol: true };

// 修改只读属性会报错
// obj1.bol = false; // Error

// 由于使用索引签名,传入额外的属性不会报错
const obj2: IObj = { str: "abc", other: "none" };


/**
 * 方法类型接口
 */
interface IMethod {
    fn1(arg: string): boolean;
}

const fnObj: IMethod = {
    // 普通方法
    fn1: function (arg: string) {
        return arg === "";
    },

    // 简写格式
    // fn1(arg: string) { return arg === ''; }

    // 箭头函数格式
    // fn1: (arg) => arg === '',
};

/**
 * 函数类型接口
 */
interface IFn {
    // 函数签名:
    // (参数:类型):返回类型
    (arg1: string, arg2: number): boolean;
}

// 函数传参可以少于函数签名定义的参数,但不能异于,
// 比如:arg1: number, arg3: boolean
let fn3: IFn = function (arg1: string): boolean {
    return arg1 !== "";
};

/**
 * 索引类型接口
 */
interface IArr {
    // 索引签名:(参数类型只能为 number | string)

    // 表示用数字类型的索引访问时,会得到字符类型的值
    [index: number]: string;

    // 访问字符类型索引时返回任意类型值,比如访问方法名
    [prop: string]: any;

    // [] 中冒号前的单词(index, prop)可任意指定,
    // 只用于阅读,类似于注释,ts 不作处理和判断
}

const arr1: IArr = ["a", "b", "c"];

// 访问数字类型索引
arr1[0]; // 'a'

// 访问字符类型索引
arr1["length"]; // 3

/**
 * class 类型接口
 */
interface IPerson {
    // 描述类的公共属性,无法包含私有属性
    firstName: string;
    lastName: string;
    age?: number;
}

// 实现接口的类
class Person implements IPerson {
    public firstName: string;
    public lastName: string;
    public age?: number;

    constructor(obj: IPerson) {
        this.firstName = obj.firstName;
        this.lastName = obj.lastName;
        this.age = obj.age;
    }
}

/**
 * 接口继承
 */
interface IOne {
    attr1: string;
}

interface ITwo extends IOne {
    attr2: number;
}

const obj3: ITwo = {
    // 少写任何一个都会报错
    attr1: "123",
    attr2: 123,
};

// 继承多个接口
interface IThree extends IOne, ITwo {
    attr3: boolean;
}

const obj4: IThree = {
    attr1: "123",
    attr2: 123,
    attr3: true,
};

/**
 * 混合类型的接口
 */

// 同时声明多种类型,为复杂结构所使用
interface IMulti {
    (arg1: number): void;
    attr1: string;
    fn1(arg: number): number;
}

// 函数类型
const fn5: IMulti = function () {};
// 属性
fn5.attr1 = "def";
// 方法
fn5.fn1 = (arg: number) => arg;

/**
 * 接口继承类
 */
class CSPub {
    // 公共属性
    public pub: string = "aaa";
}

class CSPri {
    // 私有属性
    private pri: string = "bbb";

    getPri?(): string {
        return this.pri;
    }
}

class CSPro {
    // 受保护属性
    protected pro: string = "ccc";
}

// 接口会继承类的 所有成员(只有类型声明,不包含具体实现),
// 即继承类的 公共,私有,受保护的成员
interface IClassPub extends CSPub {
    attr1: string;
}

interface IClassPri extends CSPri {
    attr1: string;
}

interface IClassPro extends CSPro {
    attr1: string;
}

// 继承了只有公共成员的类的接口,可以被任意 其他类 实现
// 通常用于复用类的约束
class CSI1 implements IClassPub {
    pub = "aaa";
    attr1 = "ddd";
}

// 具有私有成员或受保护成员的接口,只能由 继承类 实现
// 通常用于拓展对类的子类的约束
class CSI2 extends CSPri  implements IClassPri {
    attr1 = "ddd";
}
class CSI3 extends CSPro implements IClassPro {
    pro = "ccc";
    attr1 = "ddd";
}

/**
 * 接口合并
 */
interface IMerge {
    str: string;
}
interface IMerge {
    num: number;
}

// 接口可以多次重复申明,引用时会取所有申明的 并集
const mergeObj: IMerge = {
    str: 'abc',
    num: 123,
};

/**
 * 申明类
 */
class CS1 {
    attr1: string;                // 不加成员修饰默认是公共(public)
    public attr2: string = "";    // 公共属性(可以被派生类、实例对象访)
    private attr3: string = "";   // 私有属性(只能自己访问)
    protected attr4: string = ""; // 受保护属性(只能被自己和派生类访问)
    readonly attr5: string = "";  // 只读属性(只能被当前类的构造函数修改)
    static attr7: string = "";    // 静态属性(只能直接通过类访问,实例和当前类 this 都访问不到)

    /**
     * 构造函数
     *
     * 具有 protected 构造函数的类,只能被派生类访问,不能实例化(不影响派生类是否能实例化)
     * 具有 private   构造函数的类,既不能实例化,也不能被继承
     */
    constructor(arg: string, public attr6?: string) {
        /**
         * 参数属性
         *
         * public attr6: string 称为 参数属性,用于同时定义并初始化一个指定成员,
         * 通过在参数前添加成员修饰符形成,如 public, private, readonly,
         * 功能等同于:
         *
         * attr6: string;
         *
         * constructor(attr6: string) {
         *   this.attr6 = attr6;
         * }
         */

        // 获取所有属性
        this.attr1 = arg + this.attr2 + this.attr3 + this.attr4;

        // 只读属性可以在该类中修改
        this.attr5 = "1";
    }

    // 方法不加修饰符也默认是 public
    getAttr1(): string {
        // 只读属性无法在该类的方法中修改
        // this.attr5 = '2'; // Error
        return this.attr1;
    }

    // 静态方法
    static getStatic(): string {
        // 无法访问当前类(this)的属性与方法
        // this.attr1; // Error

        // 静态属性只能通过该类直接访问(this 获取不到值)
        return CS1.attr7;
    }
}


/**
 * 继承
 *
 * 被继承类称为 父类,继承类称为 派生类
 */
class CS2 extends CS1 {
    constructor(arg: string) {
        // 调用父类的 constructor 方法
        // 只要申明了构造函数,必须调用 super()
        // (不申明构造函数,则不影响该类的属性方法调用父类属性与方法)
        super(arg, "");

        // 父类的只读属性不能被派生类修改
        // this.attr5 = '1';
    }

    // 甚至可以重写父类的方法(只有该类实例化后能访问到)
    getAttr1(): string {
        return this.attr1;
    }

    getAttrs(): string {
        // 获取父类的 公共、受保护成员(获取不到私有成员)
        return this.attr1 + this.attr2 + this.attr4;
    }

    getSuperAttr1(): string {
        // 获取父类的方法
        return this.getAttr1();
    }
}


/**
 * 类的实例(实例化)
 */
const cs1 = new CS1("aaa", "a");

// 实例只能访问公共属性与方法
cs1.attr1;
cs1.attr2;
cs1.attr5;
cs1.attr6;
cs1.getAttr1();
// 修改只读属性会报错
// cs1.attr5 = '1';

// 访问类的静态属性与方法
CS1.attr7;
CS1.getStatic();

const cs2 = new CS2("bbb");

// 派生类实例只能访问 父类的公共属性与方法、派生类的公共属性与方法
cs2.attr1;
cs2.attr2;
cs2.attr5;
cs2.attr6;
cs2.getAttr1();
cs2.getAttrs();
cs2.getSuperAttr1();


/**
 * 实例的类型
 */

// 表示 cs21 应是类 CS2 或其派生类的实例
let cs21: CS1;

cs21 = new CS1('1');
cs21 = new CS2('1');

// 如果成为了其他类的实例会报错
// class CS22 {}
// cs21 = new CS22(); // Error

// 赋予其他值也会报错
// cs21 = '123'; // Error


/**
 * 抽象类
 *
 * 用作其他派生类的基类,不能被直接 实例化
 * 与接口的区别是,可以包含成员的 实现细节
 */
abstract class CS3 {
    // 可以申明普通成员并初始化值,派生类中可直接访问
    attr1: string = "111";

    /**
     * 抽象属性
     *
     * 必须在派生类中申明该属性,该基类中不会存在该属性
     * 抽象属性也能和成员修饰符结合
     */
    abstract attr2: string;
    protected abstract attr3: string;

    // 抽象类也可以有构造函数
    constructor(public name?: string) {}

    /**
     * 抽象方法
     *
     * 不能包含具体实现,但必须在派生类中实现
     */
    abstract fn1(arg: string): boolean;

    // 普通方法,可以包含具体实现
    fn2(): string {
        return this.attr1;
    }
}

class CS4 extends CS3 {
    attr2: string = '';
    attr3: string = '';

    fn1(arg: string) {
        return !!arg;
    }
}

// 抽象类的子类可以实例化
const cs4 = new CS4();

cs4.attr1; // "111"
cs4.attr2;
cs4.fn1('1'); // true
cs4.fn2(); // "111"

函数

/**
 * 函数类型定义
 */
function fn5(arg: string): boolean {
  return arg === "";
}

// 匿名函数
let fn6 = function (arg: string): boolean {
  return arg === "";
};

// 箭头函数
fn6 = (arg: string): boolean => arg === "";

// 箭头式类型定义
// 箭头前为参数类型,后为返回值类型
let fn61: (arg: string) => boolean;
fn61 = (arg) => arg === '';


/**
 * 可选参数
 */
function fn7(arg1: string, arg2?: string): boolean {
  return arg2 ? arg2 === "" : arg1 === "";
}

// 可选参数必须放到必输参数后面
// function fn8(arg2?: string, arg1: string): boolean {
//   return arg2 ? arg2 === "" : arg1 === "";
// }

fn7('a');
fn7('a', 'b');
// 参数多了会报错
// fn7('a', 'b', 'c');


/**
 * 参数默认值
 * 
 * 加默认值的参数会成为 可选参数
 */
function fn9(arg: string = 'abc'): boolean {
  return arg === '';
}

fn9(); // false
fn9(''); // true

// 默认值参数可以放到必输参数前面,但是必须传入 undefined 才能获取到默认值,不传报错
function fn10(arg1 = 'abc', arg2: string): boolean {
  return arg1 === arg2;
}

// fn10(''); // Error
fn10('', 'abc'); // false
fn10(undefined, 'abc'); // true


/**
 * 剩余参数
 * 
 * 剩余的参数可以一个没有或多个,即被当成可选参数
 */
function fn11(arg1: string, ...args: (string | number)[]): boolean {
  return arg1 === args[0];
}

fn11('a', 'b', 123);
fn11('a');


/**
 * 重载(overload)
 * 
 * 函数根据传参的不同会有不同的返回类型
 */

// 重载手段是为一个函数提供多个类型定义
function fn12(x: number[]): number;
function fn12(x: string): string;
function fn12(x: boolean): boolean;
function fn12(x: any): any {
  // 上面一行只是函数的实现签名,为了兼容上面两个重载签名,不能被直接调用,
  // 同时它也并不算作一个重载,真正的重载签名只有最上面的三个
  if (typeof x === 'object') {
    return x[0];
  } else {
    return x;
  }
}

// 都能通过类型检查
fn12([1, 2, 3]);
fn12('abc');

// 没在重载定义中的类型会报错
// fn12(undefined); // Error

泛型

// 函数返回类型与传参类型一致
function fn13(arg: number): number {
  return arg;
}

fn13(123);

// 如果传参支持多种类型,就需要写多个重载定义,如果使用 any,
// 虽然能包含所有情况,但不能保证参数和返回值类型一致;
function fn14(arg: any): any {
  return arg + '';
}

fn14(123);
fn14('abc');


/**
 * 类型变量
 */

// 使用 类型变量 可以解决这个问题
// 变量 T 用于捕获并存储用户传入的类型
// 这里的函数 fn15 便是泛型(泛型函数),即适用于多个类型的函数
function fn15<T>(arg: T): T {
  return arg;
}

// 使用泛型时在类型变量的位置传入类型值
fn15<number>(123);

// 通常是使用时省略,直接利用编译器的 类型推断 来判断类型
fn15('abc');

// 但是一些复杂类型情况下,编译器可能不能自动推断出类型,
// 这时就任然需要使用 <> 来指定类型
interface IObj2 {
  a: number;
  b: string;
};

fn15<IObj2>({ a: 123, b: 'abc' });


/**
 * 使用
 */

// 由于 T 表示任意类型,所以不能直接访问某些属性
function fn16<T>(arg: T): T {
  // return arg.toString(); // Error
  return arg
}

// 如果是复合类型,则可以使用某些固有属性
function fn17<T>(arg: T[]): string {
  return arg.toString();
}

// 类型变量可以使用其他字母或者单词(通常使用 T)
// 也可以存在多个变量
function fn18<M, My, other>(arg: M): M {
  let one: My;
  let two: other;

  return arg;
}

// 存在多个类型变量时依次指定
fn18<string, number, boolean>('abc');


/**
 * 泛型接口
 */

// 接口里的泛型
interface IGeneric {
  <T>(arg: T): T;
}

let fn19: IGeneric = function(arg) {
  return arg;

  // 和接口申明不一致会报错
  // return arg + '';
}

// 针对整个接口的泛型
interface IGeneric2<T> {
  a: T;
  b: T[];
  c(arg: T): T;
}

const obj5: IGeneric2<number> = {
  a: 123,
  b: [1, 2, 3],
  c: (arg) => arg + 1
};


/**
 * 泛型类
 */
class CS5<T> {
  constructor(public attr: T) {}

  fn(): T {
    return this.attr;
  }

  // 静态成员不能使用泛型类型
  // static a: T = '';
}

const cs5 = new CS5<number>(123);

cs5.fn(); // 123


/**
 * 泛型约束
 * 
 * 需要约束泛型具有某些属性,可以 extends 指定接口
 */

interface IObj3 {
  length: number; // 约束为具有 length 属性的任意类型
}

function fn20<T extends IObj3>(arg: T): number {
  return arg.length;
}

fn20('abc'); // 3

// 报错,因为数字没有 length 属性
// fn20(123); // Error

// 泛型约束中使用类型参数
// 表示第二个参数需要是第一个参数对象的属性
function fn21<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

fn21({ a: 1 }, 'a');

// b 不是第一个参数对象的属性
// fn21({ a: 1 }, 'b'); // Error

高级类型


/**
 * 交叉类型
 * 
 * 多个类型合并为一个类型,合并类型需同时满足多种类型
 * 类似类型的 与(&)运算
 */
interface IStr {
  str: string,
}

interface INum {
  num: number,
}

// 合并后的交叉类型
type StrAndNumType = IStr & INum;

const obj6: StrAndNumType = {
  // 缺少属性会报错
  str: 'abc',
  num: 123,

  // 多余属性也会报错
  // bol: true,
};


/**
 * 联合类型
 * 
 * 类似类型的 或(|)运算,需至少满足多种类型的一种
 */
type StrOrNumType = IStr | INum;

let strOrNum: StrOrNumType;

// 满足联合类型中任意一种 即可通过
strOrNum = { str: 'abc' };
strOrNum = { num: 123 };


/**
 * 类型守卫
 * 
 * 在运行时 检查以确保某个作用域类型 的表达式
 */

// 如果函数入参是联合类型,那么内部访问该参数时,用任意一种类型访问参数都会报错,
// 因为 ts 判断联合类型时只要有一个类型不满足,就不会通过,所以导致每个都不通过;
function fn22(arg: number | number[]): number {
  // if (arg > 0) {
  //   return arg + 1; // Error

  // } else if ( arg.length > 0) { // Error
  //   return arg.pop(); // Error

  // } else {
  //   return 0;
  // }

  // 为了避免这种情况的报错,需要使用 类型断言
  if (arg as number > 0) {
    return arg as number + 1;

  } else if ( (<number[]>arg).length > 0) {
    return (<number[]>arg).pop() as number;

  } else {
    return 0;
  }
}

// 由于多次书写类型断言很麻烦,所以 ts 出现了 类型守卫 的机制:

/**
 * typeof 类型守卫:
 */
function fn23(arg: number | number[]): number {
  // ts 会识别 typeof 语句,并将当前 if 区块中的 arg 都当成 number,
  // 类似于加上了隐式的类型断言
  if (typeof arg === 'number' && arg > 0) {
    return arg + 1;
  
  // 其他 if 区块也能识别为指定类型
  } else if (typeof arg === 'object' && arg.length > 0) {
    return arg.pop() as number;
  
  } else {
    return 0;
  }
}

/** 
 * truthiness 类型守卫
 */
function fn24(arg: number[] | null): void {
  if (typeof arg === 'object') {
    // 不能直接使用数组的特性,即使使用了 typeof,因为 null 也是 'object' 类型,
    // 并且 null 不能使用 for...of...
    // for (let i of arg) { i; } // Error

    // 使用 truthiness 类型守卫可以进一步限制变量为 数组类型,
    // !!arg 或者 Boolean() 等涉及布尔运算的操作都能触发这种类型守卫;
    if (arg) {
      for (let i of arg) { i; }
    }
  }
}

/**
 * 等式类型守卫
 */
function fn25(a: string | number, b: string | boolean): void {
  // 直接使用参数的方法会报错,因为可能是 number 类型
  // a.toUpperCase(); // Error
  // b.toUpperCase(); // Error

  // 使用了等式判断后,ts 会自动取二者的交集类型,即 string,就可以正常调用方法了,
  // 也可以使用类似 a === null 的直接与基本类型对比的判断触发类型守卫;
  if (a === b) {
    a.toUpperCase();
    b.toUpperCase();
  }
}

/**
 * instanceof 类型守卫
 */
function fn26(arg: Number | String): void {
  //(x instanceof y) 用于检查 x 的原型链中是否包含 y.prototype

  // 直接调用方法会报错,Number 类型不存在该方法
  // arg.toUpperCase(); // Error

  // instanceof 表达式会触发类型守卫,限制 arg 为 String 类型
  if (arg instanceof String) {
    arg.toUpperCase();
  }
}

fn26('abc');
// fn26(true); // Error

/**
 * 自定义类型守卫
 * 
 * 首先自定义一个函数来判断参数类型,返回值类型具有固定格式:
 * param is type
 * 其中 param 必须是参数,type 是类型
 */
function isStr(arg: string | number): arg is string {
  // 内部可以使用其他 类型守卫
  return typeof arg === 'string';

  // 也可以直接使用类型断言
  // return (arg as string).toUpperCase !== undefined;
}

function fn27(arg: string | number): void {
  // 像使用其他类型守卫一样引用 自定义函数,触发类型守卫
  if (isStr(arg)) {
    arg.toUpperCase();
  } else {
    arg += 1;
  }
}


/**
 * 类型别名
 * 
 * 为一些基本类型或复杂类型定义一个别名
 */

type MyString = string;
type StrOrNum = string | number;
type StrAndNum = string & number;
type callback = (arg: string) => boolean

// 像使用基本类型一样使用类型别名
let myString: MyString;

// 类型别名可以是 泛型
type TypeArr<T> = T[];
const strArr: TypeArr<string> = ['a'];

// 类型别名可以 自己引用自己(嵌套)
type NestObj = {
  str: string,
  obj?: NestObj,
}
const nestObj: NestObj = {
  str: 'abc',
  obj: { str: 'def' },
}


/**
 * 字面量类型
 * 
 * 使用该类型的变量值需要等于字面值
 */
type OneOrTwo = 'one' | 'two' | 3 | true;

let num: OneOrTwo;

num = 'one';
num = 'two';
num = 3;
num = true;

// 取类型中不存在的值会报错
// num = 'three'; // Error


/**
 * 可辨识联合(Discriminated Union)
 * 
 * 
 * 
 * 
 * 当联合类型中的每一个类型,都包含一个为字面量类型的共有属性时,
 * ts 将会把这个联合类型识别为 可辨识联合
 */
interface Square {
  kind: "square";
  size: number;
}
interface Circle {
  kind: "circle";
  radius: number;
}

// 该联合类型每个接口类型都含有 kind 属性,并且是字符串字面量类型,
// kind 被称为可辨识联合 Shape 的 可辨识特征(Discriminant)
type Shape = Square | Circle;

function area(s: Shape): number {
  // 直接访问会提示 Circle 没有 size 属性
  // return s.size ** 2; // Error

  // 使用 switch 或者 if 判断 kind 属性后,ts 会自动限制为指定类型
  switch (s.kind) {
    case "square":
      return s.size ** 2;

    // 但是仍然存在一个问题,漏写判断时 ts 并不会提示
    // case "circle":
    //   return Math.PI * s.radius ** 2;

    default:
      return 0;
  }
}

// 解决提示漏写 case 的问题,可以利用 ts 类型守卫的特性,
// 即代码中判断了所有联合类型的可能性之后,该联合类型会被限制为 never 类型,
// 所以在最后尝试将联合类型赋值给一个 never 类型,如果报错就说明还有情况没有判断到;
function area2(s: Shape): number {
  switch (s.kind) {
    case 'square':
      return s.size ** 2;

    case "circle":
      return Math.PI * s.radius ** 2;

    default:
      // 如果上面的 case 漏写一个,下面就会报错,
      // 提示 不能将 xxx 分配给 never;
      let result: never = s;
      return result;
  }
}


/**
 * 索引类型
 */
interface IObj4 {
  name: string,
  age: number,
}

// keyof 为索引查询操作符,表示类型是接口所有属性的 联合类型
// 等价于:
// type Key4 = 'name' | 'age';
type Key1 = keyof IObj4;
let key1: Key1;

key1 = 'name';
key1 = 'age';
// key4 = 'hobby'; // Error

// 或者获取普通对象的类型的索引
let obj7 = { name: '', age: 0 };
type Key2 = keyof typeof obj7;
let key2: Key2 = 'name';


/**
 * 映射类型
 * 
 * 将类型中每个属性进行映射转变,类似数组的 map 方法
 */
interface IObj5 {
  name: string;
  age: number;
}

type Map1 = {
  // 为接口中每个属性添加 只读与可选 标识
  readonly [K in keyof IObj5]?: IObj5[K];

  // 无法新增额外的属性
  // hobby: string; // Error
};

// 等同于:
// type Map1 = {
//     readonly name?: string;
//     readonly age?: number;
// }

// 也可以写一个通用的转变泛型
type TMap2<T> = {
  readonly [K in keyof T]?: T[K];
}

type Map2 = TMap2<IObj5>;

// ts 也内置了一些条件类型,如:

/**
 * Readonly - 属性全转换为只读:
 */
type Map3 = Readonly<{ a: string }>; // { readonly a: string }


/**
 * Partial - 属性全转换为可选:
 */
type Map4 = Partial<{ a: string }>; // { a?: string | undefined }

/**
 * Pick - 属性拷贝
 */
type Map5 = Pick<{ a: string; b: number }, 'a'>; // { a: string }

/**
 * Exclude<T, U> - 从 T 中剔除可以赋值给 U 的类型
 */
type Map6 = Exclude<'a' | 'b', 'b'>; // 'a'

/**
 * Extract<T, U> - 从 T 中提取可以赋值给 U 的类型
 */
type Map7 = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'

/**
 * NonNullable<T> - 从 T 中剔除 null 和 undefined
 */
type Map8 = NonNullable<'a' | null | undefined>; // 'a'

/**
 * ReturnType<T> - 获取函数返回值的类型
 */
type Map9 = ReturnType<() => string>; // string

装饰器

/************************************************************
 * 装饰器
 *
 * 用于标注或修改类及其成员,是一种特殊类型声明,
 * 支持被附加到类声明、方法、访问符、属性或参数上
 ************************************************************/

/**
 * 基本使用
 */

// 声明装饰器函数:
// 函数一共接收三个参数:
// 1. 装饰器所装饰的目标(target);
// 2. 装饰目标的属性名(key);
// 3. 装饰目标的属性描述值(descriptor);
function decBasic(target: unknown, key: string, descriptor: object): void {
  console.log('decorator');
}

console.log('=============== Basic ===============');

// 使用装饰器,直接写在装饰目标的上方,
// 这里装饰目标是类的 getNum 方法;
class DecBasicCS {
  @decBasic
  getNum(): void {
    console.log('class');
  }
}

const decBasicCS = new DecBasicCS();
decBasicCS.getNum();
// "decorator"
// "class"


/**
 * 装饰器工厂
 * 
 * 可自定义如何装饰一个目标(允许为装饰器传参)
 */
function decFactory(str: 'add' | 'plus') {
  // 这里可写自定义装饰逻辑
  let returnFn;

  if (str === 'add') {
    // 装饰器工厂函数最后需要返回一个装饰器函数
    returnFn = function(target: unknown, key: string): void {
      console.log('add');
    }
  } else {
    returnFn = function(target: unknown, key: string): void {
      console.log('plus');
    }
  }

  return returnFn;
}

console.log('=============== Factory ===============');

class DecFactoryCS {
  // 装饰器工厂函数可以像普通函数一样传参执行(运行时执行)
  @decFactory('add')
  useAdd(): void {}
  
  // 使用不同参数针对不同装饰目标,或使用不同装饰逻辑
  @decFactory("plus")
  usePlus(): void {}
}

const decFactoryCS = new DecFactoryCS();
decFactoryCS.useAdd(); // "add"
decFactoryCS.usePlus(); // "plus"


/**
 * 装饰器组合
 * 
 * 一个装饰目标可同时被多个装饰器叠加装饰
 */
function decMultiOne(target: unknown, key: string): void {
  console.log('one');
}
function decMultiTwo(target: unknown, key: string): void {
  console.log('two');
}

function decMultiOneFact() {
  console.log('one fact');
  return function(target: unknown, key: string): void {
    console.log('one');
  }
}
function decMultiTwoFact() {
  console.log('two fact');
  return function(target: unknown, key: string): void {
    console.log('two');
  }
}

console.log('=============== Multi decorator ===============');

class DecMultiCS {
  // 多个装饰器叠加时,会从装饰目标开始,依次向上执行,
  // 类型于执行 decMultiOne(decMultiTwo(getNum))
  @decMultiOne
  @decMultiTwo
  getNum(): void {}

  // 装饰器也可以横向书写
  @decMultiOne @decMultiTwo getNum1(): void {}

  @decMultiOne @decMultiTwo
  getNum2(): void {}

  // 使用装饰器工厂也能叠加,但是执行顺序是:
  // - 先自上而下执行工厂函数的逻辑
  // - 然后自下而上执行装饰器的逻辑
  @decMultiOneFact()
  @decMultiTwoFact()
  getNum3(): void {}
}

const decMultiCS = new DecMultiCS();
decMultiCS.getNum();
// "two"
// "one"
decMultiCS.getNum1();
// "two"
// "one"
decMultiCS.getNum2();
// "two"
// "one"
decMultiCS.getNum3();
// "one" "fact"
// "two" "fact"
// "two"
// "one"


/**
 * 多种装饰目标类型
 * 
 * 执行顺序:
 * 1. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器 应用到每个 实例成员。
 * 2. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器 应用到每个 静态成员。
 * 3. 参数装饰器应用到构造函数。
 * 4. 类装饰器应用到类。
 */
function decClass() {
  console.log('class fact');
  return function(target: Function): void {
    console.log('class');
  }
}
function decAttr() {
  console.log('attr fact');
  return function(target: unknown, key: string): void {
    console.log('attr', key);
  }
}
function decMethod() {
  console.log('method fact');
  return function(target: unknown, key: string): void {
    console.log('method', key);
  }
}
function decGetter() {
  console.log('getter fact');
  return function(target: unknown, key: string): void {
    console.log('getter');
  }
}
function decSetter() {
  console.log('getter fact');
  return function(target: unknown, key: string): void {
    console.log('getter');
  }
}
function decArg() {
  console.log('arg fact');
  return function(target: unknown, key: string, index: unknown): void {
    console.log('arg');
  }
}

console.log('=============== Multi target ===============');

// 类装饰器,实际是作用于类的构造函数,
// 即装饰器的 target 入参是构造函数(并且只能定义这一个入参)
// 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明
@decClass()
class MultiDecCS {
  /**
   * 属性装饰器
   *
   * target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
   * 属性装饰器没有第三个入参 属性描述符;
   */

  // 公共属性
  @decAttr()
  public publicAttr: number = 0;

  // 私有属性
  @decAttr()
  private privateAttr: number = 0;

  // 受保护属性
  @decAttr()
  protected protectedAttr: number = 0;

  // 静态属性
  @decAttr()
  static staticAttr: number = 0;

  /**
   * 方法装饰器
   *
   * target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
   * 如果方法装饰器返回一个值,它会被用作方法的属性描述符
   */
  @decMethod()
  getVar(): void {}

  // 静态方法
  @decMethod()
  getStaticVar(): void {};

  /**
   * getter 修饰器
   *
   * target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
   * 但不能同时向同一个属性的 getter 和 setter 设置修饰器
   * 如果访问器装饰器返回一个值,它会被用作方法的属性描述符
   */
  @decGetter()
  get var(): undefined { return; }
  
  /**
   * setter 修饰器
   */
  @decSetter()
  set var1(val: unknown) {}

  /**
   * 参数装饰器
   *
   * target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
   * 入参第三个是当前参数在入参数组中的索引
   */
  getArg<T>(@decArg() arg: T): T { return arg; }
}

const multiDecCS = new MultiDecCS();