3. TypeScript类型深入 - 《TypeScript入门与实战》读后总结

导读

  • TS内部工作原理与方式,不了解也不影响使用

正文

子类型特性

类型系统可靠性

  1. 子类型与父类得关联关系就是里氏替换原则:任何超类(父类)使用到的地方可以用子类替换
  2. 可靠的类型系统:能够识别并拒绝程序中所有的类型错误
  3. TS 提供了 两种方式选择性降低可靠性,但目的都是用来适配JS旧的编码模式
  • const a:string = (1 as unknown) as string;
  • 一些编译选项 如:--strictNullChecks

子类基本特性

  1. A:>A && A:<A [自反性,自己是自己本身的超类或子类]
  2. 传递性 A:>B:>C => A:>C

各个类型间子类关系的盘点

1. 顶端类型

  • 所有类型都是顶端类型(unknown,any) 的子类
  • 尾端类型(never)是所有类型的子类

2. 字面量类型

  • 字面量类型是所有对应原始类型的子类,undefined 是 never 以外所有类型的子类
  • null 是 never和 undefined 外所有类型的子类

3. 枚举类型

  • enum E = {A=0,B},E.A:>E [枚举成员类型是枚举类型的子类型]
  • E :> number

4. 函数类型

  • 同时考虑,参数类型和返回值类型,由于参数类型和返回值很有可能是一个复杂的类型组成(变形)
什么是变形
  • 变形(复杂类型与组成类型间的子类关系)[A:>B A是B的子类]
  1. A:>B => Complex(A) :> Complex(B) [协变]
  2. A<:B => Complex(A) :> Complex(B) [逆变]
  3. A:>B || A<:B => Complex(A) :> Complex(B) [双变]
  4. 没有变形 [不变]
函数父子类关系判定
函数参数 [参数的判定逻辑和返回值不太一样]
  1. 参数数量 [Snb <= Tnb]
    /**
     * 如果函数S的必选参数在函数T里都能找到
     * 则 S 可以是 T的子类
     * 
     * */
    type a = (x: string) => void;
    type b = (x: string, y: number) => void;
    type c = (x?: string, y?: number) => void;
    type d = (...xx: Array<number | string>) => void;
    type e = (x: string,...xx: Array<number | string>) => void;

    let aa: a = (x: string) => { };
    let aaa: a;
    let bb: b = (x: string, y: number) => { };
    let bbb: b;
    let cc: c = (x?: string, y?: number) => { };
    let ccc: c;
    // 全部都是剩余参数也不可靠
    let dd: d = (...xx: Array<number | string>) => { };
    let ddd: d;
    let ee: e = (x: string,...xx: Array<number | string>) => { };
    let eee: e;

    // b的必选参数没有在a中找到对应,所以 a 是 b的子类,b不是a的子类
    aaa = bb;//error
    bbb = aa;
    aaa = cc;
    ccc = aa;//error
    // 函数之间是不是子类首先看子类必选参数值是否能在父类中找到
    aaa = dd;
    ddd = aa;//error
    // 剩余参数不计入必选参数总数,所以有父子类关系
    aaa = ee;
    eee = aa;
  1. 参数类型 [逆变]
    /**
     * 参数相当于一个函数的功能
     * 参数范围扩大了,相当于函数的功能扩展了(既保留父类的功能,又扩展了新的功能)
     * 功能扩展相当于在父类的基础上扩充的子类
     * 所以是逆变关系
    */
    // 严格模式下,函数对应参数的类型与函数之间的类型是逆变关系
    type S = (x:number) => void;
    type T = (x:0|1) => void;

    let s:S = (x:number) =>{};
    let ss:S;
    let t:T = (T:0|1) =>{}
    let tt:T;
    // 严格模式下,逆变关系  
    // 由于S的参数类型<:T的参数类型,所以由于逆变只能推导出 S:>T  
    ss = t; // error
    tt = s;
    //非严格模式下,双变关系
    // 如果 复杂类型 S:>T
    // => params(S):>params(T) || params(T):>params(S)
函数返回值 [协变]
     // 函数的返回值类型
    // 无论是严格模式还是非严格模式,函数的返回值类型和函数类型都是协变关系
    // b(return):>a(return) => b:>a
    type a = ()=>number;
    type b = ()=>0|1;
    let aa:a = ()=>10;
    let aaa:a;
    let bb:b = ()=>1;
    let bbb:b;
    aaa=bb;
    bbb=aa;// error
重载函数子类关系
    // 函数重载之间的子类关系
    // 需要对应每一个重载函数能找到对应的子类
    /**
     * 若 S:》T
     * 1. 参数数量之间是 Snb<=Tnb
     * 2. 参数类型之间是逆变关系
     * 3. 返回值类型之间是协变关系
     */
    type S = {
        (x: string): string,
        (x: number): number
    }
    type T = {
        (x: 'a'): string,
        (x: 1): number
    }
    //重载函数的函数实现,参数为并集,返回值为交集
    let ss: S = (x: number | string): never => { throw Error() };
    let sss:S;
    let tt: T = (x: 'a'|1): never => { throw Error() };
    let ttt:T;
    sss = tt;//error
    ttt = ss;

5. 对象类型 [由零个或者多个对象组成,需要考虑到每一个对象成员]

  • 结构化子类型
    • 对象间的子类关系取决于对象的结构,与声明「构造函数无关」—— 结构化子类型
    class A {
        x:number = 1;
        y:string = 'y';
    }
    class B {
        x:number = 1;
        y:string = 'y';
    }
    let a:A = new A();
    let b:B = new A();
    a = b;
    b = a;
  • 属性成员类型
  1. 父类的成员
    • (必选+非必选)必须能在子类中找对对应的 [类似逆变]
  2. 成员数量 [Sn >= Tn]
    /***
     * S 是 T的子类
     * T 的(非必选)成员能在 S找到对应的 Tn<=Sn
     * 如果T中是必选那么S中也是必选
     * S 的成员数量不可小于 T的成员
     * */
    interface A {
        x:number
    }
    interface B{
        x:number,
        z:number
    }
    let a:A = {x:1};
    let b:B = {x:1,z:2};
    
    a = b; // B 是 A的子类
    b = a; // error 
  1. 成员类型 [协变]
    /***
     * S 是 T的子类
     * S 中的每一个必选成员N
     * 是T中对应成员 M的子类
     * */
    interface A {
        x:number
    }
    interface B{
        x:1,
    }
    let a:A = {x:1};
    let b:B = {x:1};
    
    a = b; // B 是 A的子类
    b = a; // error 
  1. 调用签名
    /** 
     * S 是 T的子类
     * T中每一个调用M
     * 在S中能找到一个调用N
     * N:>M
     */
  1. 构造签名
    /**
     * T中每一个构造M
     * 在S中能找到一个构造N
     * N:>M
     */
    type A = {
        (x: number): void,
        (x: number, y: string): void
    
    }
    type B = {
        (x: number, y?: string): void
    }
    interface Aa {
        new (x: number): void,
        new (x: number, y: string): void
    
    }
    interface Bb {
        new (x: number, y?: string): void
    }
    // B:> A
    // Bb:> Aa
  1. 字符串索引签名
    • 子类也是字符串索引签名,且成员类型为协变
  interface T {
      [x: string]: number
  }
  interface S {
      [x:string]: 1
  }
  let tt:T = {
      x:111
  }
  let ss:S = {
      y:1
  }
  tt = ss;
  ss = tt;// error
  1. 数值索引签名
    • 子类可以是字符串索引签名,或者数值索引签名,成员类型为协变关系
    • 子类的范围可以大,但是不能小
    interface T1 {
        [x: number]: boolean
    }
    interface S1 {
        [x:number]: true,
        [y:string]:true
    }
    let ttt:T1 = {
        1:false
    }
    let sss:S1 = {
        y:true,
        2:true
    }
    ttt = sss;
    sss = ttt; //error        

6.类类型

    /**
     * 1. 仅检查实例成员类型
     * 2. 不检查静态成员类型和构造函数类型
     * 3. 如果存在私有成员和受保护成员,必须来自同一个类(意味着两者要存在着继承的关系)
     * */
    // 对于父类A来说,其中实例成员的每一个都能在子类B的实例成员中找到
    // 且 是A的子类
    // B:>A
    class A {
        x: number = 1;
        y: string = 'a';
        constructor(p: number) { }
    }
    class B {
        x: number = 2;
        y: string = 'b';
        z: number = 3;
        static t: string = '';
        constructor(p: boolean) { }
    }
    // B:>A
    class A {
        protected x:number = 1
    }
    class B extends A{
        y: number = 2;
    }
    let aa:A = new A();
    let bb:B = new B();
    aa = bb;
    bb = aa;// error

7.泛型类型

泛型对象类型
  • 指泛型接口,泛型类,表示对象类型的类型别名
  • 拥有泛型类型参数的泛型对象无法判定子类关系,只有泛型类型参数实例化后才能用对象类型的子类关系判定
    1. 先将泛型对象进行实例化
    2. 再判断实例化后的对象子类关系
    interface F<T>{
        a:T,
        b:string
    }
    interface G<T>{
        a:T
    }
    let ff:F<'1'> = {a:'1',b:'b'};
    let fff:F<'1'>;
    let ffff:F<string>;
    let gg:G<boolean> = {a:false};
    let ggg:G<boolean>;
    let gggg:G<string>;
    fff = gg; // error
    ggg = ff; // error
    ffff = gg; //error
    gggg = ff; // 根据泛型类型参数不同子类关系就不同
泛型函数类型
  • 非严格模式 [子类关系判定]
    type A = <T,U>(x:T,y:U)=>[T,U];
    type B = <S>(x:S,y:S)=>[S,S];
    1. 统一泛型参数为any
    2. A = (x:any,y:any)=>[any,any];
    3. B = (x:any,y:any)=>[any,any];
    4. 结论 A:>B B:>A 
  • 严格模式
    type A = <T,U>(x:T,y:U)=>[T,U];
    type B = <S>(x:S,y:S)=>[S,S];
  • 子类关系判定思路
    1. 如果推断两个泛型函数,由于由泛型类型参数的存在,无法判断AB之间的子类关系(同泛型对象类型的子类关系判定)
    2. 泛型函数类型参数的本质描述的是(函数参数)和(返回值) 之间的关系,要进行子类推断首先要进行(类型参数统一)
    3. 步骤:先假设AB之间的子类关系,根据(函数参数)子类关系统一泛型类型参数
    4. 为什么要根据(参数)子类关系,而不是(返回值)子类关系去统一泛型类型参数呢
    5. 因为参数是一一对应的,可以进行具体的类型赋值(子类可以赋值给父类),而返回值只是一个类型,无法一一对应参数,无法准确推断参数类型
    6. 所以假设泛型函数的子类关系后,要用父类的参数类型推断子类的参数类型,即用(父类的泛型类型参数)去表达(子类泛型类型参数)
    7. 用父类类型参数表达子类类型参数的原因是,泛型函数的子类关系和参数子类关系是(逆变关系)
    8. 所以推断泛型函数的步骤为:
    9. 假设泛型函数的子类关系
    10. 看泛型函数参数的必选参数个数是否满足对应关系(子类的必选参数必须在父类中能找到)
    11. 根据泛型函数的子类关系推断出泛型函数参数的父子类关系(逆变)
    12. 根据参数一一对应原则,用父类的参数类型表达子类的参数类型
    13. 统一类型参数后看泛型函数的子类关系和返回值的子类关系是否满足(协变关系)
    14. 满足则该假设成立,不满足则不成立
  • 子类关系的具体步骤
    • 一:判断A是否为B的子类,即 A:>B
      1. 假设 A:>B A为B的子类
      2. 由(参数)逆变关系统一泛型参数类型然后看(返回值)子类关系是否符合(协变关系)
      3. 由于函数参数和函数之间的子类关系是逆变关系,即 A:>B => params(A)<:params(B)
      4. 由于A为B的子类,所以参数 S:>T && S:>U A的对应参数为B的超类
      5. 所以S 对应的参数可以赋值给U也可以赋值给T
      6. 统一 type A = <S>(x:S,y:S)=>[S,S];type A= type B ,两者互为子类
      7. 所以假设 A:>B A为B的子类成立
    • 二:判断B是否为A的子类,即 A<:B
      1. 先假设 B:>A B为A的子类
      2. 由(参数)逆变关系统一泛型类型参数,然后看(返回值)的子类关系是否符合协变关系
      3. 由于函数参数与函数之间的子类关系是逆变关系,即 A:>B => params(A)<:params(B)
      4. 由于B是A的子类,所以参数 S<:T && S<:U => S = U|T ,所以S对应的参数可以由 U|T 赋值
      5. 统一 type B =<U,T>(x:U|T,y:U|T) => [U|T,U|T];
      6. 可见统一后,B的返回值不一定是A的返回值,所以该假设不成立
      7. 所以假设不成立,B不是A的子类
  • 示例例举
    // 1
    type X = <T,U>(x:T,y:U)=>[T,U];
    type Y = <S>(x:S,y:S)=>[S,S];
    var a:X = <T,U>(x:T,y:U):[T,U]=>{
        return [x,y];
    }
    var aa:X;
    var b:Y = <S>(x:S,y:S):[S,S]=>{
        return [x,x];
    }
    aa = b;//error
    b = aa 
    // 2
    type Z = <T,U>(x:T,y:U)=>U|T;
    type C = <S>(x:S)=>S;
    // Z:>C 否,参数数量就不符合
    // Z<:C type C = <T>(x:T) => T; 是
    var zz:Z;
    var zzz:Z = <T,U>(x:T,y:U):T|U=>{
        if(x){
            return x;
        }else{
            return y;
        }
    }
    var cc:C;
    var ccc:C= <S>(x:S):S=>{
        return x;
    }
    zz = ccc;
    cc = zzz;// error
    // 3
    type G = <T,U>(x:T&U,y:U)=>T;
    type P = <S>(x:S,y:S)=>S;
    // G:>P G=<T,S>(x:S,y:S)=>S 成立
    // G<:P T&U|U = U, P=<U>(x:U,y:U)=>U ;U不一定是T的子类,所以不成立
    var gg:G;
    var ggg:G = <T,U>(x:T&U,y:U):T=>{
        return x;
    }
    var pp:P;
    var ppp:P=<S>(x:S,y:S):S=>{
        return x;
    }
    gg = ppp // error
    pp = ggg;

8.联合类型

    S= S0 | S1 
    若 T:>S0 || T:>S1 => T:>S
    若 S0:>T && S1:>T => S:>T

9.交叉类型

    S = S0 & S1
    若 S0:>T || S1:>T => S:>T
    若 T:>S0 && T:>S1 => T:>S 

兼容性

  • 赋值兼容性
    1. 若 S:>T => T = S // 绝大多数情况
    2. 应用:函数调用语句中,实参与形参需要满足赋值语句
  • 子类兼容性
    • 绝大所属,子类能赋值给兼容类,S是T的子类,但有以下三种例外
    // any 不是子类,但能够赋值给任意类型
    let x: any;
    let ass: number = x;

    // 数值类型不是 number类型的子类
    let nn: number = 1;
    enum E { A, B };
    let ee: E;
    ee = nn;

    // 由于 Sn > Tn 所以S 不是 T的子类
    type S = { x: number, y?: string };
    type T = { x: number };

    let a: S = { x: 1, y: '' };
    let aa: S;
    let b: T = { x: 2 };
    let bb: T;
    aa = b;
    bb = a;

类型推断

    /**
     * 类型推断
     * 每一个表达式都有一个类型
     * 表达式的类型来源有
     * 1. 类型注解
     * 2. 类型推断
     */
    // 【类型推断的分类】
    // 1. 常规类型推断
    let x = 0; // x=> number
    const y = 0;// y=> 0
    // 2. 最佳类型类型推断
    // 推断出最小(精确)范围的夫类型
    class A{}
    class d extends A {};
    class c extends A {};
    const rr = [new d(),new c(),new A()]// rr=>A[]
    const rr = [new d(),new c()]// rr=>(d|c)[]
    const rr = [new d(),new c()] as A[]// 编译器推断不是想要的类型,直接用类型断言指定
    const rr:A[] = [new d(),new c()]// 编译器推断不是想要的类型,直接用类型断言指定
    // 3. 上下文类型推断,从前往后推断
    interface add{
        (x:number,y:number):string
    }
    let aaa:add = (x,y)=>''+x+y;//由于上下文 推断出x,y均为number,aaa返回值为string

类型放宽 [TS内部行为,将放宽的类型作为推断结果]

常规类型放宽

  1. 非严格模式:将所有undefined和null放宽为any类型
    const a = undefined // any
    const a = null // any
  1. 不会放宽 undefined 和 null
    const a = undefined // undefined
    const a = null // null

字面量类型放宽 [将字面量类型放宽为原始基础类型]

可放宽字面量类型

  • 类型源自类型
let z = 0; // z=> number

不可放宽字面量类型

  • 类型原子表达式
let z:0 = 0; // z=> 0

可放宽字面量类型

    true => boolean
    1 => number 
    '' => string
    2n => bigint
    枚举成员 => enum
    ['a',1]的元素(字面量的联合类型) => string|number (字面量类型的联合类型)

字面量类型放宽场景

    let a = 0;//numebr
    const b = 0// b为可放宽类型,但是不会执行放宽操作,推断结果为 0
    const f = [0];//number[]
    const f2 = [0,''];//(number|string)[]
    const g = {a:0,b:''}//{a:number ,b:string}
    class F{
        a=0;//number
        readonly b=0;//0
    }
    function ff(a=0){a;}//a=>number 形参指定默认
    function ff2(){return 1;}//return number

是否全新属性与是否可放宽属性

  • 只有新的字面量类型,才可以执行放宽操作
  • 每个字面量类型都有[内置属性]表示是否放宽
  • 每个类型源自表达式的字面量类型都有一个[内置属性]表示是否为全新字面量
  • 只有全新的字面量类型才能执行放宽操作
    const a = 0;//0 a 全新可放宽,但不可变,所以推断值为0
    let b = a;//numebr // b可变,b全新可以放宽,b的类型为表达式,a为非全新但为可放宽=>b可放宽

    const c:0 = 0;//c 为全新不可放宽
    let d = c;// 0 d是全新,d类型源自表达式,c为非全新,不可放宽=>d不可放宽

    let e = 0;//number
    let f = 0 as const; //0 f虽为全新但是const不执行放宽操作

命名空间

命名空间意义

  • 为了解决JS没有模块支持,组织隔离代码避免命名冲突

命名空间本质

    namespace Utils {
    function aaa(): string {
        return '';
    }
    }
    // =》
    "use strict";
    var Utils;
    (function (Utils) {
        function aaa() {
            return '';
        }
    })(Utils || (Utils = {}));

命名空间基本用法

    namespace System {
    namespace Utils {//嵌套
        function aaa(): string {
        return '';
        }
        export interface Point {
        x: number,
        y: number
        }
    }
    export namespace Common {
        let x: number = 1;
        function y(): void { }
        export function y2(): void { }//导出
        import Point = Utils.Point;// 类型导入/导出
        const p: Point = { x: 1, y: 2 };
        y();
    }
    }
    namespace Tools {
    import y2 = System.Common.y2;
    y2();//导入
    ccc();//用到了 b.ts 中的ccc函数,a.ts依赖于b.ts,保证b.ts先加载(定义依赖关系),不然可能未定义错误
    }
    Tools.ccc(); //不同文件同名命名空间会合并
    console.log(System);
    console.log(System.Common);
    // console.log(System.Common.y);//error
    console.log(System.Common.y2);

命名空间定义文件之间的依赖关系

  1. 定义配置文件,b依赖于 a,a编译后先与b加载
    {
    "files":["a.ts","b.ts"],
    "compilerOptions": {
        "outFile": "main.js",
        // "module": "commonjs",////Only 'amd' and 'system' modules are supported alongside  
    }
    }
  1. 三斜綫指令優先級高於tsconfig.json
    // a.ts
    /// <reference path="b.ts"/> //三斜线指令,指定依赖b.ts
    // a.ts
    namespace App {
        export function iss(): void { }
    }
    //b.ts
    /// <reference path="a.ts" />
    namespace App {
        const a = iss();
    }
    // tsconfig.json
        "files":["b.ts"],
        "compilerOptions": {
        "outFile": "main.js",
        }
    // TSC
    "use strict";
        var App;
        (function (App) {
            function iss() { }
            App.iss = iss;
        })(App || (App = {}));
        /// <reference path="a.ts" />
        var App;
        (function (App) {
            const a = App.iss();
        })(App || (App = {}));

命名空间总结

  • 优先选择模块代替命名空间

模块

是什么

  • 模块:将程序按照功能划分为独立可交互的模块

模块简史及ESM介绍

  • CommonJS:exports,服务器端模块系統

    • AMD:
      1. CJS采取同步加载不适于浏览器,会造成阻塞,
      2. 采取AMD,define注冊模块,require申明依赖,exports导出
    • UMD:
      1. CJS不能在浏览器,AMD也不能在服务器使用,UMD基于AMD并对CJS进行适配
      2. 本质就是把两者兼容了一下,做了一个判断
  • ESM:十年后的官方标准,import,export关键字

  • 导出

    • 命名模块导出:export var a=0;
    • 命名模块导出列表:export {a,b}
    • 默认导出L:export default function a(){}
    • 聚合模块导出: export {a,b} from 'mod'
    • 重命名:
    • export {oldM as newM}
    • export {oldM as newM} form './mod'
  • 导入

    • import {a,B,C} from 'mod'
    • 全部:import * as A from 'mod'
    • 默认:import X from 'mod'
    • 空导入(只是执行代码) import 'mod'
    • 重命名:
    • import {oldM as newM} from 'mod'
    • 副作用:
    • 某个操作对外部环境产生影响,
    • 模块尽量要保持隔离,
    • 但是某些模块设计上就是全局作用域交流沟通的,监听全局事件,设置全局变量

类型的模块导入导出

TS类型的导入导出两种方式

ESM模块的导入导出 import/export
  • 当导入导出类和枚举是,既可以当作值使用,又可以当作类型使用,因为枚举和类即可作为值又可以作为变量
  • 这种方式不能导出纯类型,会报错
只针对类型的导入导出 import/export type
  • 无论导入导出什么,是值还是类型,都只导出类型,不能当作值使用
示例
    // a.ts
    {
    /**
     * 导入/导出方式一
     * ESM导入/导出
     * 这种方式只能导入/导出 一个值
     * 枚举和类型既可以当作值又可以当作类型
     */
    export enum A {
        a, b, c, d
    }
    export class B {
        bb: string;
        constructor() {
        this.bb = 'dd';
        }
    }
    export function z(x: number):number {
        return x
    }
    // type Si = string | number;
    // export  String; // error 这种方式是错误的,不能用只导出值得方式导出纯类型

    /**
     * 导入/导出方式二
     * 只针对类型得导入/导出,需要类型关键字 type 申明
     * 这种方式只能导入/导出类型,即便是
     */
    export type Si = string | number;
    export type nn = '1';
    export interface Point { // interface 本质也相当于类型申明type了
        x: number,
        y: number
    }
    export class C {
        cc: number;
    }
    export enum X { x, y, z }
    // export type function y(x: number):number //error 函数字面量类型是无法导出的
    export type y = {():number}//只能用对象字面量的调用属性表达函数
    }
    // b.ts
    {
    import { A, B, z } from './a';
    // 既可以当作类型又可以当作值
    // A
    let a: A = 1;
    console.log(A.b);
    // B
    const b = new B();
    const f: B = b;
    // z 只能当作值使用
    z(1);
    // 只能用作类型
    import type { Si, nn, Point, C, X, y } from './a';
    const ss: Si = 1;
    // C(); // error 用作值会失败
    X.x;//error
    let xx:X = 1;
    y();//error
    let yy:y = ():number=>{
        return 1;
    }
    }

在类型的导入导出中只会导入导出变量的类型,而不会导入导出变量的值

默认情况下,JS编译器会删除导入导出语句中类型相关的语句

  1. 模块导入标识符仅仅被用在类型的位置上
  2. 模块的导入导出标识符没有被用在表达式的位置上,没有作为值使用
    // a.ts 
    export const a: number = 1;
    export interface Point {
        x: number,
        y: number
    }
    // b.ts
    //An import path can only end with a '.ts' extension 
    // when 'allowImportingTsExtensions' is enabled.
    import { Point } from './a';//不用带 .ts
    const p: Point = { x: 1, y: 2 };
    // 编译后 
    // a.js Point只表示类型所以被删除了
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.a = void 0;
    exports.a = 1;
    // b.js Point没有作为值使用,所以被删除了
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    const p = { x: 1, y: 2 };

针对类型的导入、导出带有副作用(某个操作对外部环境产生影响)的代码将不会执行

    // tsconfig.json
    "include": ["./**/*.d.ts"],
    // global.d.ts
    declare var globalThis:{mode:string};
    // a.ts 不会执行
    globalThis.mode = 'mod';
    // b.ts
    import { Point } from './a';
    const p: Point = { x: 1, y: 2 };
    if (globalThis.mode === 'mod') {
        console.log('>>>> :', p)
    }

导出类型方式

    import type { Point } from 'mod';
    import type as *  from 'mode'

精确控制导入导出

"importsNotUsedAsValues":"remove",// remove,preserve,error

  • remove:删除类型相关,
  • preserve:保留所有import语句,
  • error:发现可以改写为import type的会报错

动态模块导入优化性能

    setTimeout(()=>{
        import('./b').then(res=>{
        console.log('res>>>',res);
        })
    },200)

生成指定格式的代码

tsc a.ts --module CommonJS // AMD UMD ...

外部申明

外部声明用法

// global.d.ts
    /**
     * 外部申明
     * 不允许定义初始值
     * 值表示一个类型
     */
    declare var ggg:{mode:string};
    declare var b:number;
    declare type A = string;
    // 函数声明
    declare function  f(a:string):void;
    // 类型申明,要在代码种运行还要给出具体的定义
    declare class C{
        public static s0():string;
        private static s():string;
        public a:number;
        constructor(any:number);
        get c():number;
        set c(value:number);
        [index:string]:any;
    }
    /***
     * 枚举肯定有初始值
     */
    declare enum M {
        A = 'a',
        B = "b"
    }
    declare const enum N {
        U = 1,
        V
    }
// .a.ts
    b = 1;
    ggg.mode = '';//运用的话,必须初始化
    const cc = new C(1);//运用的话必须初始化
    //编译是不会报错,但是运行会报错,外部申明只申明全局变量类型
    console.log('CC>>>', cc);
    // 外部声明枚举,不用加 type,
    /**
     * 如果需要外部申明枚举值,
     * 最好用const枚举类,不然会出现奇怪的错误
     */
    enum M { //因为 已经申明了 M 枚举值,但M不是常量枚举,所以这里还要初始化一便,不然使用M属性会报错
        C = 7, D
    }
    let x: M = M.A //这里只是能获取M的枚举类型,但是如果真正使用还是要初始化
    let Y: M = M.C;
    console.log('x.A>>>', x) // undefined // 因为M不是常量枚举值,所以虽然编译之前不报错,但是使用的时候还是没值
    console.log('y.C>>>', Y) // 7
    // enum N { // N如果已经是const了。,这里就不可以申明了
    //   C = 7, D
    // }
    let x2: N = N.U;
    let Y2: N = N.V;
    console.log('x2.U>>>', x2) // 1
    console.log('y2.V>>>', Y2) // 2

外部申明命名空间

// golbal.d.ts
    declare namespace Foo {
    /**
     * 外部申明
     * 不允许定义初始值
     * 值表示一个类型
     */
    declare var ggg: { mode: string };
    declare var b: number;
    declare type A = string;
    // 函数声明
    declare function f(a: string): void;
    // 类型申明,要在代码种运行还要给出具体的定义
    declare class C {
        public static s0(): string;
        private static s(): string;
        public a: number;
        constructor(any: number);
        get c(): number;
        set c(value: number);
        [index: string]: any;
    }
    }
// a.ts
    const x:Foo.A = '';

外部模块申明[用于TS导入JS模块使用,为了给JS模块进行类型申明]

//a.ts
    /**
     * 在ts文件中引入js文件,由于js代码中没有类型约束,
     * 所以ts无法获得js代码的类型信息,进而会隐式推断js中类型为any
     * ,失去了ts代码类型推断和约束的作用,
     * 声明文件就是将一个js模块中所有对外暴露的变量、函数、类使用ts语法进行类型声明,
     * 进而让ts编译器在检测到该声明文件之后(只要被检测到写在哪都可以)
     * 就可以获取js文件中对应变量、函数、类的类型信息
     * 引用: https://blog.csdn.net/z1625000762/article/details/127346324
     */
    import { read } from './ss.js';
    const s: String = read('xx');
    console.log(s);
// global.d.ts
    declare module '*/ss.js' {
    export function read(f: string): string;
    export const b:number; //申明模块的变量/class必须是const类型
    }
// ss.js
    module.exports = {
        read: (f) => {
            return f;
        },
        b: 1
    } 

申明文件来源

  • TypeScript内置的申明文件
    1. TS安装目录下的lib文件中 lib.[description].d.ts 文件\
    2. 定义了标准的JS api
    3. 定义了特定运行环境的api DOM Api Web Workers Api
    4. 在TS中可以使我们直接使用这些API
  • 第三方申明文件
    1. 第三方代码包中已经包含的申明文件 比如通常是 index.d.ts
    2. npm包有标准的 package.json 文件 中有types/typing属性,定义了申明文件
    3. package.json 申明文件兼容TS语言版本 typesVersions
    4. 第三方包中没有定义但是在 DefinedlyTyped中能找到包的申明文件
    5. DefinelyTyped (http://DefinitelyTyped.org) 是一个公开集中式的申明文件代码仓库(现在网站已经不维护了)
    6. 如果第三方包没有定义申明文件可以去 DefinitelyTyped 去搜素(直接去npm搜素,DefinitelyTyped网站没了)
    7. DefinitelyTyped 仓库中所有的申明文件会发布到npm的@types中
  • 自定义申明文件
    1. 以上方式均找不到包的申明文件需要自己定义
    2. import * as $ from 'jquery' 可以自己详尽定义一个模块,也可以用于跳过第三方代码库的类型检查

模块解析

相对模块导入

import b from './b' //相对文件路径 / ./ ../

非相对模块导入

import a from 'b' //没有文件路劲符号就是非相对路径导入

需要指定模块解析策略

  • --moduleResolution Classtic
    1. 尝试将模块视为一个文件解析
    2. 阶段
      • 相对
        1. 在相对文件夹中 ts/tsx -> d.ts -> js/jsx
      • 非相对,多了一层 node_moudles
        1. 文件解析过程 ts/tsx -> d.ts -> js/jsx
        2. 目录解析过程 ./ ,./node_moudles -> 母文件夹 ../,../node_moudles
        3. 直至根目录
  • --moduleResolution Node (默认)功能更丰富
    • 相对
      1. 在相对文件夹中 将模块视为文件,查找ts/tsx/d.ts
      2. 在相对文件夹中 将模块视为目录,查找 package.json 的 types/typing 属性接着查找文件
      3. 在相对文件夹中 将模块视为文件,查找 js/jsx
      4. 在相对文件夹中 将模块视为目录,查找 package.json 的 main属性 接着查找文件
    • 非相对
      1. 视为文件,在./node_moudles 查找 ts/tsx
      2. 视为目录,在./node_moudles 查找 package.json 的 types/typing
      3. 视为申明文件,在./node_moudles/@types查找
      4. 重复1-3 直至根目录
      5. 视为文件,在./node_moudles 查找 js/jsx
      6. 视为目录,在./node_moudles 查找 package.json 的 main
      7. 重复 5-6 直至系统根目录

设置--baseUrl:

  1. 支持命令行和tsconfig
  2. 执行tsc所在目录解析过程根据 moduleResolution

设置 paths:

  1. 设置模块名和模块路径的映射
  2. path:{"@bar/":["bar/"]}
  3. 只支持tsconfig
  4. 基于 baseUrl ,必须同时设置

设置 rootDirs

1.  使用不同目录创建出虚拟目录
2.  "rootDirs":["bar","foo"] 
3.  相对导入 ./b 时会同时查找 bar 和 foo

导入外部申明模块

1. 使用非相对模块导入,就会导入外部模块
2. import * as mod from 'mod'

设置 --traceResolution 打印出具体的模块导入步骤

申明合并

如何看待

  • 了解这是TS的一种特性,提供一种解决问题的思路,具体规则代码会给出提示,不做深入研究,具体问题具体分析

是什么

  1. TS标识符能够表示 值.类型,命名空间
  2. 函数申明和类申明能创建出新的命名空间
  3. 申明合并时TS特有的行为
  4. 按照标识符含义合并,值和值,类型和类型,命名空间和命名空间

接口申明合并

  1. 合并多个属性
  2. 相同属性不同类型会出现编译错误
  3. 同名函数会被重载,后声明的具有更高优先级,参数包含字面量类型的具有更高优先级
  4. 所有接口只允许存在一个字符串索引签名和数字索引签名
  5. 应该避免复杂接口的合并行为

枚举申明合并

  1. 必须在多个同名枚举定义初始值,因为多个同名枚举值只能为第一个枚举类型自动计算枚举值

类申申明合并

  1. 外部类可与接口合并,不支持类与类的合并

命名空间申明合并

  • 命名空间与命名空间合并
    • 非导出成员不会合并,内层命名空间也可合并。
  • 命名空间与函数合并
    • 函数必须位于命名空间之前
  • 命名空间与类合并
    • 类必须申明与命名空间之前
  • 命名空间与枚举合并
    • 命名空间和枚举的成员不允许出现同名成员

扩充模块申明

  • 不能在模块中增加新的顶层申明,只能扩充现有申明
    // a.ts
    import {B} from './b';
    declare module './b' {
    interface B{
        y:number
    }
    }
    const bb:B = {
    x:1,
    y:2
    }
    // b.ts
    export interface B {
    x: number
    }

扩充全局申明

    export {} // 导出空语句,因为全局对象扩充语句必须在模块或者外部模块中申明
    declare global {
        interface Window {
        my: Object
        }
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容