typeScript 知识汇总

101 阅读11分钟

小知识点: 如何在ts中扩展window /global (主流环境已经可以用globalThis 了,不管是浏览器 还是node环境)

const EmployeeWindow = (window || global || globalThis) as any; // 在变量之下创建需要的属性即可 
EmployeeWindow.runCypherQuery = () => { // body }
  1. 执行方式

node下执行需用tsc转换为js文件,然后执行js文件(node xxx.js)
deno(https://www.denojs.cn/)环境下可以直接执行ts文件,使用命令deno run xxx.ts`
  1. 类型种类

2.1  可以通过typeinterface 定义类型 ,当然还有enum(枚举类型)
   
2.2  typeScipt 的类型系统主要包含以下几个类别:
-  stringnumberboolean、null、undefined、symbolobjectArrayObjectStringNumber;
-  unknown(未知类型)、any;
-  never(永不返回, 是所有类型的字类型);
-  interface(接口类型)、type(类型别名);
-  unions(联合类型)、intersection(交叉类型);
-  enum(枚举类型)。
  1. 类型推断

ts 中声明变量与赋值同时进行,有类型推断,不能随便更改类型
例如:

    let a = 9;  // 类型推断为number
    a = 'zzz' //以下赋值不允许, 提示 不能将类型“string”分配给类型“number”。ts(2322)

4. ### type与interface的区别:

  • type 为类型别名,不仅可以用来表示基本类型,还可以用来表示对象类型、联合类型、元组和交集
  • 扩展类型的方式不同:type通过联合类型&实现, interface通过extends
  • 是否允许重复定义:interface允许重复定义,属性可以覆盖或增加,type不可以重复定义
  1. 名称解释:交叉类型,联合类型,元祖

交叉类型 & : type NewType = Type1 & Type2     //表示类型都必须匹配到Type1,Type2
联合类型 | :  type NewType = Type1 | Type2     //表示类型匹配到 Type1 Type2中的至少一个就可以
元祖: 就是ts中一种数组的类型,数组的元素个数确定,并且每个元素的类型确定,不能更改(数组个数和元素类型)
  1. 类泛型的接口类型定义

该写法notice.......

    interface IPerson<T> {
      new(...args: unknown[]): T;
    }
  1. 函数定义类型的方式

函数作为参数传值的时候用到,或者 定义变量声明为函数时候用到

tips:下面注意使用interface 与 type 写法的区别

 写法1interface F{
        (a:number): number   // 指定返回值number类型。如果指定返回类型为void ,表示可以不用返回值
     }      
 写法2type F = (a:number) => number  // number为返回值类型
    
    使用方式如下:
    function show(f:F) {
         const r = f(2)
         alert(r)
    }
    const c:F = (n: number) => {
        return n + 2
    }    
    show(c)
  1. 函数泛型:适应了参数的类型可能广泛(不确定)的情况

function identity <T>(value: T) : T { 
    return value;  // 此处value不能参与运算 , 因为T类型不确定
}

function identity <T, U>(value: T, message: U) : T { 
    console.log(message); 
    return value; 
}

对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。下面我们来看张动图,直观地感受一下类型传递的过程:

generic-type-filled

  1. enum类型

enum 枚举类型tips 1:
    枚举类型是一种含有多个成员组成的数据类型,只要属于其中的成员之一,便是该枚举类型。 
    换句话说,指定为该枚举类型,则必须是其成员之一。
enum 枚举类型tips 2:
    枚举类型使用计算值赋值的情况,必须放到其它成员后面,例如:
    enum Count {
        n1,               // 默认值为0
        n2 = getN2Value()
    }    
    以下形式说错误的
    enum Count {
        n2= getN2Value(), // wrong
        n1,
    }
enum 枚举类型tips 3:
    含字符串值成员的枚举中不允许使用计算值,例如:
    enum Count {
        n1 = "n1",
        n2 = getN2Value() // 不允许
    }    
    
  1. 泛型约束

所谓泛型约束,通俗点来讲就是约束泛型需要满足的格式。有一个非常经典的实例:

// 定义方法获取传入参数的length属性
function getLength<T>(arg: T) {
  // throw error: arr上不存在length属性
  return arg.length;
}

这里,我们定义了一个 getLength 方法,希望函数获取传入参数的 length 属性。

因为传入的参数是不固定的,有可能是 string 、 array 、 arguments 对象甚至一些我们自己定义的 { name:"zzzzzz", length: 9 },所以我们为函数增加泛型来为函数增加更加灵活的类型定义。

可是随之而来的问题来了,那么此时我们在函数内部访问了 arg.length 属性。但是此时,arg 所代表的泛型可以是任意类型。

比如我们可以传入一个 boolean ,那么此时函数中的泛型 T 代表 boolean 类型,访问 boolean.length ? 这显然是一个 bug 。

那么如果解决这个问题呢,当然就提到了所谓的泛型约束 extends 关键字。

我们先来看看如何使用它:

interface IHasLength {
  length: number;
}

// 利用 extends 关键字在声明泛型时约束泛型需要满足的条件
function getLength<T extends IHasLength>(arg: T{
  return arg.length;
}

getLength([123]); // correct
getLength('123'); // correct
getLength({ name'zzzzzz', length9 }); // correct
// error 当传入true时,TS会进行自动类型推导 相当于 getLength<boolean>(true)
// 显然 boolean 类型上并不存在拥有 length 属性的约束,所以TS会提示语法错误
getLength(true); 
  1. keyof使用

接受一个对象类型作为参数,并返回该对象所有 key 值组成的 联合类型。

interface IProps {
  name: string;
  age: number;
  sex: string;
}

// Keys 类型为 'name' | 'age' | 'sex' 组成的联合类型
type Keys = keyof IProps

// Keys 类型为 string | number | symbol 组成的联合类型
type Keys = keyof any

例如,我们希望实现一个函数。该函数希望接受两个参数,第一个参数为一个对象object,第二个参数为该对象的 key 。函数内部通过传入的 object 以及对应的 key 返回 object[key] 。

function getValueFromKey(obj: objectkeystring) {
  // throw error
  // key的值为string代表它仅仅只被规定为字符串 
  // TS无法确定obj中是否存在对应的key
  return obj[key];
}

显然,我们直接为参数声明类型这是会报错的。采取如下方式:

// 函数接受两个泛型参数
// T 代表object的类型,同时T需要满足约束是一个对象
// K 代表第二个参数K的类型,同时K需要满足约束keyof T (keyof T 代表object中所有key组成的联合类型)
// 自然,我们在函数内部访问obj[key]就不会提示错误了
function getValueFromKey<T extends object, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
  1. ts中extends 与 immplents的使用

extends使用方式:

  • 1)class中使用extends 可以继承其它类,但不能继承接口,可以使用immplents继承接口 接口使用extends 继承其它接口
  • 2)泛型约束:本质就是继承 (参考10中的例子),
     function getObjValue<T extends object, K extends keyof T>(obj: T, k: K) {
        return obj(k)
     }
  • 3)条件判断 (extends是条件类型关键字)
    格式  SomeType extends OtherType ? TrueType : FalseType;

例如:

      type A1 = 'x' extends 'x' ? string : number;         // string
      type A2 = 'x' | 'y' extends 'x' ? string : number;   // number

对于使用extends关键字的条件类型(即上面的三元表达式类型),如果extends前面的参数是一个泛型类型, 当传入该参数的是联合类型,则使用分配律计算最终的结果。

分配律是指,将联合类型的联合项拆成单项, 分别代入条件类型,然后将每个单项代入得到的结果再联合起来,得到最终的判断结果*

      type A3 = P<'x' | 'y'>        //  string | number的联合类型
      type P<T> = T extends 'x' ? string : number;   

所以A3 根据分配律相当于泛型中的类型分别计算结果组成的联合类型(也就是P<'x'> | P<'y'>)

特殊的never

    // never是所有类型的子类型 (也就是never extends 任何类型 判断都是true)
      type A1 = never extends 'x' ? string : number; // string

      type P<T> = T extends 'x' ? string : number;
      type A2 = P<never> // never 这里为什么是never

never被认为是空的联合类型,也就是说,没有联合项的联合类型, 所以还是满足上面的分配律,然而因为没有联合项可以分配, 所以P<T>的表达式其实根本就没有执行, 所以A2的定义也就类似于永远没有返回的函数一样,是never类型的。

#####如何防止条件判断中的分配呢?

方式:中括号把泛型T包起来

  type P<T> = [T] extends ['x'] ? string : number;
  type A1 = P<'x' | 'y'> // number
  type A2 = P<never> // string 因为never是所有类型的子类型
  1. 断言

    - 类型断言

        类型断言会告诉编译器,你不用给我进行检查,相信我,他就是这个类型
        有2种方式:
        -   尖括号
        -   as:推荐
        //尖括号
       let num:any = '小杜杜'
       let res1: number = (<string>num).length; // React中会 error
    
       // as 语法
       let str: any = 'Domesy';
       let res: number = (str as string).length;
    
    但需要注意的是:尖括号语法在**React**中会报错,
    原因是与`JSX`语法会产生冲突,所以只能使用**as语法**
    

    - 确定赋值断言

     因为ts需tsc静态类型编译,编译过程并不会执行脚本文件。
     通过确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值
     例如:
     
        let x!: number;    // 确定赋值断言,让tsc知道,不然会报错
        initialize();
        console.log(2 * x); // Ok
    
        function initialize() { 
          x = 10;
        }
    

    - 非空断言

    在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 `!` 
    可以用于断言操作对象是非 null 和非 undefined 类型。
    
        function getUserName(name: string | null | undefined) {
           const nameStr: string = name! 
           console.log('name', nameStr);
        }
       //getUserName('JH')   
       getUserName(undefined) 
       getUserName(null)
       
       但变成`ES5`后 `!`会被移除,所以当传入 `null` 的时候,还是会打出 `null`   
    
  2. 父子类型

    让我们来写一个简单的父子类型:

    interface Animal {
      age: number
    }

    interface Dog extends Animal {
      bark(): void
    }

Dog 继承于 Animal,拥有比 Animal 更多的方法。因此我们说 Animal 是父类型,Dog 是它的子类型。需要注意的是,子类型的属性比父类型更多、更具体:

  • 在类型系统中,属性更多的类型是子类型。
  • 在集合论中,属性更少的集合是子集。

在联合类型中需要注意父子类型的关系,因为确实有点「反直觉」。'a' | 'b' | 'c' 乍一看比 'a' | 'b' 的属性更多,那么 'a' | 'b' | 'c''a' | 'b' 的子类型吗?其实正相反,'a' | 'b' | 'c''a' | 'b' 的父类型,因为前者包含的范围更广,而后者则更具体。

type Parent = "a" | "b" | "c";
type Child = "a" | "b";

let parent: Parent;
let child: Child;

// 兼容
parent = child

// 不兼容,因为 parent 可能为 c,而 c 无法 assign 给 "a" | "b"
child = parent
  • 父类型比子类型更宽泛,涵盖的范围更广,而子类型比父类型更具体
  • 子类型一定可以赋值给父类型
  1. 协变和逆变

    • 协变:  允许子类型转换为父类型
    • 逆变:  允许父类型转换为子类型

我们都清楚 TS 属于静态类型检测,所谓类型的赋值是要保证安全性的。

  • 协变 :通俗来说也就是多的可以赋值给少的,子类型可以赋值给父类型

    例如:

    let animal: Animal = { age: 12 };
    
    let dog: Dog = {
      age: 12,
      bark: () => {}
    };
    
     animal = dog
    // 兼容,能赋值成功,这就是一个协变
   
    dog = animal
    // 不兼容,会抛出类型错误:
    Property 'bark' is missing in type 'Animal' but required in type 'Dog'   
  • 逆变
    let fn1!: (a: string, b: number) => void;
    let fn2!: (a: string, b: number, c: boolean) => void;
    
    fn1 = fn2; // TS Error: 不能将fn2的类型赋值给fn1
    
    

针对于 fn1 声明时,函数类型需要接受两个参数,换句话说调用 fn1 时我需要支持两个参数的传入分别是 a:stringb:number

同理 fn2 函数定义时,定义了三个参数那么调用 fn2 时自然也需要传入三个参数。

那么此时,我们将 fn2 赋值给 fn1 ,我们可以思考下。如果赋值成功了,当调用 fn1 时,其实相当于调用 fn2 。

但是,由于 fn1 的函数类型定义仅仅支持两个参数 a:stringb:number 即可。但是由于我们执行了 fn1 = fn2

调用 fn1 时,实际相当于调用了 fn2 函数。但是类型定义上来说 fn1 满足两个参数传入即可,而 fn2 是实打实的需要传入 3 个参数。

那么此时,如果执行了 fn1 = fn2 当调用 fn1 时明显参数个数会不匹配(由于类型定义不一致)会缺少一个第三个参数,显然这是不安全的,自然也不是被 TS 允许的。

那么反过来呢?

let fn1!: (a: string, b: number) => void;
let fn2!: (a: string, b: number, c: boolean) => void;

fn2 = fn1; // 正确,被允许

按照刚才的思路来分析,我们将 fn1 赋值给 fn2 。fn2 的类型定义需要支持三个参数的传入,但实际 fn2 内部指针已经被修改称为 fn1 的指针。

fn1 在执行时仅仅需要两个参数 a: string, b: number,显然 fn2 的类型定义中是满足这个条件的(当然它还多传递了第三个参数 c:boolean,在 JS 中对于函数而言调用时的参数个数大于定义时的参数个数是被允许的)。

自然,这是安全的也是被 TS 允许赋值。

就比如上述函数的参数类型赋值就被称为逆变,参数少(父)的可以赋给参数多(子)的那一个。看起来和类型兼容性(多的可以赋给少的)相反,但是通过调用的角度来考虑的话恰恰满足多的可以赋给少的兼容性原则。

参考文章