typescript 的一些知识点 (持续补充)

120 阅读11分钟

为什么需要 typescript

简单来说就是因为JavaScript是弱类型, 很多错误只有在运行时才会被发现
而TypeScript提供了一套静态检测机制, 可以帮助我们在编译时就发现错误

静态类型

这里还有纠正一个概念,TypeScript 是静态弱类型语言,这跟C语言是一样的,并不是所谓的强类型,因为要兼容 JavaScript, 所以 TypeScript 几乎不限制 JavaScript 中原有的隐式类型转换,它对类型的隐式转换是有容忍度的,而真正的静态强类型语言比如 Java、C# 是不会容忍隐式转换的。

优点

  • 代码的可读性和可维护性:举个🌰看后端某个接口返回值,一般需要去network看or去看接口文档,才知道返回数据结构,而正确用了ts后,编辑器会提醒接口返回值的类型,这点相当实用。
  • 在编译阶段就发现大部分错误,避免了很多线上bug
  • 增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等

缺点

  • 有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等前端工程师可能不是很熟悉的概念
  • 会增加一些开发成本,当然这是前期的,后期维护更简单了
  • 一些JavaScript库需要兼容,提供声明文件,像vue2,底层对ts的兼容就不是很好。
  • ts编译是需要时间的,这就意味着项目大了以后,开发环境启动和生产环境打包的速度就成了考验

枚举类型

枚举类型是很多语言都拥有的类型,它用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型。

数字枚举

当我们声明一个枚举类型 如果没有赋值 它们就会有默认的值 默认是从 0开始

enum Direction {
    Up,
    Down,
    Left,
    Right
}

console.log(Direction.Up === 0); // true
console.log(Direction.Down === 1); // true
console.log(Direction.Left === 2); // true

如果我们只给第一个枚举值赋值 后面的值会累加

enum Direction {
    Up = 10,
    Down,
    Left,
    Right
}

console.log(Direction.Up, Direction.Down, Direction.Left, Direction.Right); // 10 11 12 13

字符串枚举

枚举类型的值其实也可以是字符串类型:

enum Direction {
    Up = 'Up',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right'
}

console.log(Direction['Right'], Direction.Up); // Right Up

异构枚举

如果一个枚举里面有 数字和字符串 我们称为 异构枚举

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

反向映射

enum Direction {
    Up,
    Down,
    Left,
    Right
}

console.log(Direction.Up === 0); // true
console.log(Direction.Down === 1); // true
console.log(Direction.Left === 2); // true
console.log(Direction.Right === 3); // true


enum Direction {
    Up,
    Down,
    Left,
    Right
}

console.log(Direction[0]); // Up

枚举的本质

被编译后的 枚举类型

我们可以把枚举类型看成一个JavaScript对象,而由于其特殊的构造,导致其拥有正反向同时映射的特性。

var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 10] = "Up";
    Direction[Direction["Down"] = 11] = "Down";
    Direction[Direction["Left"] = 12] = "Left";
    Direction[Direction["Right"] = 13] = "Right";
})(Direction || (Direction = {}));

enum Direction {
    Up = 10,
    Down,
    Left,
    Right
}

console.log(Direction[10], Direction['Right']); // Up 13

常量枚举

枚举其实可以被 const 声明为常量的,

const enum Direction {
    Up = 'Up',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right'
}

const a = Direction.Up;

枚举合并

分开声明枚举,他们会自动合并

enum Direction {
    Up = 'Up',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right'
}

enum Direction {
    Center = 1
}

// 编译后
var Direction;
(function (Direction) {
    Direction["Up"] = "Up";
    Direction["Down"] = "Down";
    Direction["Left"] = "Left";
    Direction["Right"] = "Right";
})(Direction || (Direction = {}));
(function (Direction) {
    Direction[Direction["Center"] = 1] = "Center";
})(Direction || (Direction = {}));

为枚举添加静态方法

借助 namespace 命名空间,我们甚至可以给枚举添加静态方法。

enum Month {
    January,
    February,
    March,
    April,
    May,
    June,
    July,
    August,
    September,
    October,
    November,
    December,
}

function isSummer(month: Month) {
    switch (month) {
        case Month.June:
        case Month.July:
        case Month.August:
            return true;
        default:
            return false
    }
}
namespace Month {
    export function isSummer(month: Month) {
        switch (month) {
            case Month.June:
            case Month.July:
            case Month.August:
                return true;
            default:
                return false
        }
    }
}

console.log(Month.isSummer(Month.January)) // false

interface

TypeScript 的核心原则之一是对值所具有的结构进行类型检查,它有时被称做“鸭式辨型法”或“结构性子类型化”。

在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

接口的使用 interface

这个接口 User 描述了参数 user 的结构,当然接口不会去检查属性的顺序,只要相应的属性存在并且类型兼容即可。

interface User {
    name: string
    age: number
    isMale: boolean
}

const getUserName = (user: User) => user.name

可选属性 ?

? 表示这个属性可能是 undefined

interface User {
    name: string
    age?: number
    isMale: boolean
}

只读属性 readonly

利用 readonly 我们可以把一个属性变成只读性质,此后我们就无法对他进行修改

interface User {
    name: string
    age?: number
    readonly isMale: boolean
}

函数类型 XXX:(xxx:string) => void

直接在 interface 内部描述函数:

interface User {
    name: string
    age?: number
    readonly isMale: boolean
    say: (words: string) => string
}

另一种方法,我们可以先用接口直接描述函数类型:

interface Say {
    (words: string) : string
}

interface User {
    name: string
    age?: number
    readonly isMale: boolean
    say: Say
}

可索引类型 [name: string]: string

User 还包含一个属性,这个属性是 User 拥有的邮箱的集合,但是这个集合有多少成员不确定,应该如何描述

{
    name: 'xiaozhang',
    age: 18,
    isMale: false,
    say: Function,
    phone: {
        NetEase: 'xiaozhang@163.com',
        qq: '1845751425@qq.com',
    }
}
 
{
    name: 'xiaoming',
    age: 16,
    isMale: true,
    say: Function,
    phone: {
        NetEase: 'xiaoming@163.com',
        qq: '784536325@qq.com',
        sina: 'abc784536325@sina.com',
    }
}

它们的 phone 数量不一致 但是都是字符串类型 如何定义 interface

interface Phone {
    [name: string]: string
}

interface User {
    name: string
    age?: number
    readonly isMale: boolean
    say: () => string
    phone: Phone
}

继承接口 extends

extends

可以集成多个 接口

interface VIPUser extends User, SupperUser {
    broadcast: () => void
}

类继承 implements

class 类可以通过 implements 关键字来使用 interface 的类型

interface User {
  name: string
  age: number
}

class U implements User {
  name = '1'
  age = 1
}



type Person = {
  name: string
  age: number
}

class U implements Person {
  name = '1'
  age = 1
}

泛型

泛型的作用

当我们的输入和输出 类型有所关联 我们就可以通过 泛型解决

泛型的含义

泛型是 TypeScript 中非常重要的一个概念,因为在之后实际开发中任何时候都离不开泛型的帮助,原因就在于泛型给予开发者创造灵活、可重用代码的能力。

我们现在的情况是,我们在静态编写的时候并不确定传入的参数到底是什么类型,只有当在运行时传入参数后我们才能确定。

那么我们需要变量,这个变量代表了传入的类型,然后再返回这个变量,它是一种特殊的变量,只用于表示类型而不是值。

这个类型变量在 TypeScript 中就叫做「泛型」。

function returnItem<T>(para: T): T {
    return para
}

在函数名称后面声明泛型变量 ,它用于捕获开发者传入的参数类型(比如说string),然后我们就可以使用T(也就是string)做参数类型和返回值类型了。

多个类型参数

定义泛型的时候,可以一次定义多个类型参数,比如我们可以同时定义泛型 T 和 泛型 U:

function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

泛型变量

function getArrayLength<T>(arg: Array<T>) {
  
  console.log((arg as Array<any>).length) // ok
  return arg
}

泛型接口

泛型也可用于接口声明,以上面的函数为例,如果我们将其转化为接口的形式。

interface ReturnItemFn<T> {
    (para: T): T
}

那么当我们想传入一个number作为参数的时候,就可以这样声明函数:

const returnItem: ReturnItemFn<number> = para => para
interface Dog<T, U, G> {
  name: T
  color: U
  size: G
}

const d: Dog<string, string, number> = {
  name: "小黑",
  color: "红色",
  size: 10
}
console.log(d);

泛型类

泛型除了可以在函数中使用,还可以在类中使用,它既可以作用于类本身,也可以作用与类的成员函数。

我们假设要写一个栈数据结构,它的简化版是这样的:

class Stack {
    private arr: number[] = []

    public push(item: number) {
        this.arr.push(item)
    }

    public pop() {
        this.arr.pop()
    }
}
// 同样的问题,如果只是传入 number 类型就算了,可是需要不同的类型的时候,还得靠泛型的帮助。
class Stack<T> {
    private arr: T[] = []

    public push(item: T) {
        this.arr.push(item)
    }

    public pop() {
        this.arr.pop()
    }
}

const s = new Stack<number | string | boolean>()
s.push(1,'hellow',false) 
s.push(1,'hellow',{}) // error 不能将对象传入 number string boolean 的数组中 

泛型约束

现在有一个问题,我们的泛型现在似乎可以是任何类型,但是我们明明知道我们的传入的泛型属于哪一类,比如属于 number 或者 string 其中之一,那么应该如何约束泛型呢?

type params = string | number

class Stack<T extends params> {
    private arr: T[] = []

    public push(item: T) {
        this.arr.push(item)
    }

    public pop() {
        this.arr.pop()
    }
}

const stack1 = new Stakc<string>()
const stack2 = new Stakc<boolean>()  //不满足 params 

泛型约束与索引类型

我们先看一个常见的需求,我们要设计一个函数,这个函数接受两个参数,一个参数为对象,另一个参数为对象上的属性,我们通过这两个参数返回这个属性的值,比如:

function getValue<T extends object, U extends keyof T>(obj: T, key: U) {
  return obj[key] // ok
}

使用多重类型进行泛型约束

利用 & 符号

interface FirstInterface {
  doSomething(): number
}

interface SecondInterface {
  doSomethingElse(): string
}

class Demo<T extends FirstInterface & SecondInterface> {
  private genericProperty: T

  useT() {
    this.genericProperty.doSomething() // ok
    this.genericProperty.doSomethingElse() // ok
  }
}

泛型与 new

我们假设需要声明一个泛型拥有构造函数,比如:

function factory<T>(type: T): T {
  return new type() // This expression is not constructable.
}

编译器会告诉我们这个表达式不能构造,因为我们没有声明这个泛型 T 是构造函数,这个时候就需要 new 的帮助了。

function factory<T>(type: {new(): T}): T {
  return new type() // ok
}

参数 type 的类型 {new(): T} 就表示此泛型 T 是可被构造的,在被实例化后的类型是泛型 T。

泛型小结

泛型(Generics),从字面上理解,泛型就是一般的,广泛的。

泛型是指在定义函数、接口或类的时候,不预先指定具体类型,而是在使用的时候再指定类型。

泛型中的 T 就像一个占位符、或者说一个变量,在使用的时候可以把定义的类型像参数一样传入,它可以原封不动地输出

泛型在成员之间提供有意义的约束,这些成员可以是:函数参数、函数返回值、类的实例成员、类的方法等。

用一张图来总结一下泛型的好处:

Array 和 tuple 的区别

  1. 数组一般只限制集合的类型 但是不会限制你某个位置的元素的类型 只要符合 集合类型限制就可以
let arr: (string | number | boolean)[] = ['1', 1, false]
arr[0] = 1
console.log(arr);
  1. 元祖 tuple 可以限制 集合中 每个位置的类型
let tuple: [string, number, boolean] = ['1', 1, false]
tuple[0] = 1 // err  元祖中 第一个元素必须是 string
tuple[0] = '12313'
console.log(arr);

void 和 never 类型的 区别

  1. void 类型 代表返回值 是空 只要不写 return 语句都可以 或者不返回东西

    void 类型 可以返回 undefined 但是不能返回 null

  2. never 类型 表示 不能有任何返回值 这就需要在函数中 直接报错

let a = (): void => {

}

a = (): never => {
  throw Error()
}

unknown 和any 的区别

  1. any 类型 是任何类型可以直接使用 但是要尽量不使用 因为使用any过多 会导致 失去 ts 的类型检查意义

  2. unknown 类型 代表我们不知道他是什么类型 但是他有类型 无法直接使用

  3. 要使用 unknown 类型 的话 需要配合 类型断言的方式来使用 as

let a: any = 1
a = '1'
a = true
a = {}
a = () => { }
a = class { }

let b: unknown = 333
let c: number = b as number

interface 和 type 的区别

type和interface的相同点

都是用来定义 对象 或者 函数 的形状

   interface example {
        name: string
        age: number
    }
    interface exampleFunc {
        (name:string,age:number): void
    }
    
    
    type example = {
        name: string
        age: number
    }
    type example = (name:string,age:number) => void

它俩也支持 继承,并且不是独立的,而是可以 互相 继承,只是具体的形式稍有差别

对于interface来说,继承是通过 extends 实现的,而type的话是通过 & 来实现的,也可以叫做 交叉类型

    type exampleType1 = {
        name: string
    }
    interface exampleInterface1 {
        name: string
    }
    
    
    type exampleType2 = exampleType1 & {
        age: number
    }
    type exampleType2 = exampleInterface1 & {
        age: number
    }
    interface exampleInterface2 extends exampleType1 {
        age: number
    }
    interface exampleInterface2 extends exampleInterface1 {
        age: number
    }

type和interface的不同点

首先聊聊type可以做到,但interface不能做到的事情

  1. type可以定义 基本类型的别名,如 type myString = string
  2. type可以通过 typeof 操作符来定义,如 type myType = typeof someObj
  3. type可以申明 联合类型,如 type unionType = myType1 | myType2
  4. type可以申明 元组类型,如 type yuanzu = [myType1, myType2]

接下来聊聊interface可以做到,但是type不可以做到的事情

interface可以 声明合并,示例如下

如果是type的话,就会是 覆盖 的效果,始终只有最后一个type生效

    interface test {
        name: string
    }
    interface test {
        age: number
    }
    
    /*
        test实际为 {
            name: string
            age: number
        }
    */