相关文章推荐
阳刚的熊猫  ·  C#报错: ...·  1 年前    · 
体贴的可乐  ·  前端 - ...·  1 年前    · 

看到身边的朋友在工作中都用上了 TypeScript ,都直呼真香!而我从学习和做 TypeScript 的项目到现在也有几个月了,也做了一些总结。本文和你们一起分享我的总结!

TypeScript编译器

通常情况下,编译器拿到一段代码后,会转换成抽象句法树( AST )。然后把 AST 转成字节码。字节码再传给运行时程序计算。最终得到结果。步骤如下:

  • 把程序解析成 AST
  • AST 编译成字节码。
  • 运行时计算字节码。 TS并不是直接编译成字节码,而是先编译成JS代码。然后像以上步骤那样,在浏览器或 node 运行得到JS代码。
  • 说到这,可能有些同学就问了。 TS 是怎么保证代码安全的呢?

    至关重要的一个步骤就是:TS编译器生成 ATS 后,在真正运行代码之前,TS会对代码做类型检查。TS的编译过程大致如下图。

    TSC 把TS编译成JS代码时,是不会考虑类型的。类型只在类型检查这步使用。

    类型系统分两种。

  • 通过显式句法告诉编译器所有值的类型。
  • 自动推导值的类型。 这两种类型系统TS都具备,可以显式注解类型,也可以让TS推导多数类型。
  • // 显式注解类型
    let num: number = 1;
    let isShow: boolean = false;
    let name: string = "图图";
    // 自动推导值的类型
    let n = 2;  // n是一个数字
    let b = true; // b是一个布尔值
    let m = ["美美", "牛爷爷"]; // m是一个字符串数组
    

    TypeScript类型大全

    下面列出一个TS中的类型结构图。

    any是个兜底类型(表示所有类型),声明any类型的变量后,你可以对它做任何的操作。就跟平时写的JS代码没区别。

    let a: any = 1
    a = false
    a = '图图'
    a = [1, 2, 3, 4]
    a = a.join('')
    a = a.split('')
    

    如果使用any类型,类型检查器就发挥不了其作用,导致运行时抛出错误。我们要尽量避免使用any类型。

    unknown

    unknown类型是为了解决any类型的缺陷,unknown也可以表示所有值。当你不知道一个值的类型时,又不使用any的情况下,可以使用unknown类型。但TS会要求你做二次检查。

    let num: unknown = 1 // unknown
    let isNum = num === 2 // boolean
    let addNum = num + 100 // Error: Object is of type 'unknown'
    // 检查num的类型是什么
    if (typeof num === 'number') {
      let a = num + 100 // number
      console.log(d) // 101
    

    以上示例中,unknown类型的值可以做比较(isNum)。但是不能直接性对值进行算术运算操作(addNum)。要先检查这个值的确是某个类型(a),上面使用了typeof运算符对num变量的值进行二次确认。除了typeof运算符,还可以用instance运算符。

    boolean

    boolean类型只有两个值:truefalse

    let isShow = true
    let isHide: boolean = false
    

    number

    number类型包括所有数字。

    let num: number = 1
    let addNum = 2
    

    bigint

    bigint类型是JS原有的类型。处理比较大的数时才用到。

    let big1 = 1234n
    let big2: bigint = 5678n
    

    string

    let name = '图图' // string
    let sex: string = '男' // string
    

    symbol

    symbol类型是ES6加入的类型。在实际开发中,不太常用。没学ES6的同学,建议看阮一峰老师的ES6书籍。

    let names: symbol = Symbol('美美')
    let height = Symbol('180')
    const weight: unique symbol = Symbol('55') // typeof weight
    let g: unique symbol = Symbol('55') // A variable whose type is a 'unique symbol' type must be 'const'
    

    TypeScript为symbol进行了补充,加入了unique symbol类型。在定义这种类型的值时,只能用const而不是let。在Vs Code编辑器中会显示typeof VariableName,而不是unique symbol

    null和undefined

    在Typescript中,nullundefined也有各自的类型。类型名称也是nullundefined

    这两个类型比较特殊,在TypeScript中,undefined类型只有undefined一个值,null类型只有null一个值。

    let a: undefined = undefined
    let b: null = null
    

    never和void

    除了undefinednull以外,还有nevervoid类型。这两个类型都有着明确的作用。

  • void是函数没有显式返回任何值时的返回类型。
  • never是函数根本不返回时使用的类型。(比如函数执行过程中报错了,或者永远运行下去)
  • // 返回void的函数
    function addNum(): void {
      let a = 1 + 1
      let b = a * a
    // 返回never的函数
    function a() {
      throw TypeError('总是报错')
    // 返回never的函数
    function b() {
      while (true) {
        console.log('我在无限循环')
    

    如果说unknown是其他每个类型的父类型,那never就是其他每个类型的子类型。可以把never理解成 “兜底类型”。这就说明,never类型可赋值给其他任何类型,在任何地方都可以放心使用never类型的值。

    需要注意的是,在使用never类型时。也要像unknown类型那样,对其进行二次检查。

    元组array的子类型,用于定义数组的一种特殊方式,长度固定,索引位置上的值具有固定的已知类型。声明元组时必须显式注解类型。因为创建元组用的句法和数组一样,都是方括号。TS遇到方括号,都会认为是数组类型。

    let nums: [number] = [1]
    let personInfo: [string, number, string] = ['图图', 24, '1998']
    

    元组也支持可选元素剩余元素

    // 火车票价数组,不同的路程价格不同
    let nums: [number, number?][] = [[100], [200, 259], [300]]
    // 字符串列表,至少有一个元素
    let names: [string, ...string[]] = ["图图", "小美", "牛爷爷", "图爸爸"]
    // 元素类型不同的列表
    let list: [number, boolean, ...Object[]] = [1, true, { type: "Object" }, { name: "图图" }]
    

    枚举是一种无序数据结构,用于列举该类型中的所有值。把key(键)映射到value(值)中。枚举有两种形式:字符串跟字符串之间的映射和字符串跟数字之间的映射。

    enum Fruits {
      Apple,
      Banana,
      Orange
    console.log(Fruits.Apple) // Apple
    console.log(Fruits['Orange']) // 2
    

    访问枚举的值有两种方式,方括号[]点号.都可以。不同的方式访问,所得到的值也不同。

    TS还会自动推算枚举中每个成员对应的数字,也可以自己手动设置。

    enum Car {
      Audi = 1,
      Honda = 2,
      ToYoTa = 3
    console.log(Car[1]) // Audi
    console.log(Car.ToYoTa) // 3
    

    一个枚举可以分几次声明,TS会自动将每一部分合并在一起。

    enum Fruits {
      Apple = 0,
      Banana = 1,
      Orange = 2
    enum Fruits {
      Watermelon = 3
    console.log(Fruits)
    // ts-node在控制台输出的结果
    //   '0': 'Apple',
    //   '1': 'Banana',
    //   '2': 'Orange',
    //   '3': 'Watermelon',
    //   Apple: 0,
    //   Banana: 1,
    //   Orange: 2,
    //   Watermelon: 3
    

    但要注意的是,分开声明的话TS只会推导其中一部分的值。最好给枚举的每个键都显式赋值。如果把上面的Watermelon的值去掉,它的值就为0

    键的值可以通过计算得出,而且不必给所有的键都赋值。TS会推导出缺失的值。

    enum Fruits {
      Apple = 10,
      Banana = 10 + 1,
      Orange
    console.log(Fruits)
      '10': 'Apple',
      '11': 'Banana',
      '12': 'Orange',
      Apple: 10,
      Banana: 11,
      Orange: 12
    

    上面代码中,Orange键没有赋值。TS自动推导出Orange的值为12,它的前一个键的值为11。这种行为只能是值为数字类型的情况下才会出现。

    let nums = [1, 2, 3]
    let names: string[] = ['图图', '牛爷爷', '图妈妈']
    // 泛型语法
    let fruits: Array<string> = ['apple', 'banana', 'orange']
    

    TS中的对象类型表示对象的结构。JS一般采用结构化类型,TS直接沿用。

    结构化类型:只关心对象有哪些属性,而不管属性使用什么名称。也叫做鸭子类型(即不以貌取人)。

    TS中声明对象类型有几种方式。

    object

    第一种,把一个值声明为object类型:

    let a: object = {
      b: '1111'
    console.log(a.b) 
    // Error Property 'b' does not exist on type 'object'
    

    这种方式声明一个对象,只能表示该值是个对象。而做不了任何操作。

    对象字面量

    第二种,让TS推导该对象的结构。也就是对象字面量句法。

    let person = {
      name: '图图',
      age: 24
    // ts推导出的类型
    // person: {
    //   name: string;
    //   age: number;
    

    也可以在花括号内明确描述。

    let person: { name: string, age: number } = {
      name: '小美',
      age: 18
    // ts推导出的类型
    // person: {
    //   name: string;
    //   age: number;
    

    对象字面量句法的意思是:这个东西的结构是这样的。

    { name: string, age: number }描述的是一个对象的结构,上面的例子中的对象字面量满足了该结构,如果添加额外的属性或缺少必要的属性时,就会报错。

    let person: { name: string, age: number } = {
      name: '图图',
      age: 24
    person.height = 175
    // Error Property 'height' does not exist on type '{ name: string; age: number; }'
    // 类型{ name: string; age: number; }上不存在height属性
    

    默认情况下,TS对对象的属性要求非常严格。如果声明对象有个类型为number的属性ageTS将预期对象有这么一个属性,而且也只有这一个属性。如果缺少age属性,或者多了其他属性,就会报错。

    针对这种情况,后面会讲到可选属性索引签名

    空对象类型

    对象字面量表示还有一种:空对象类型({})。除了nullundefined以外的任何类型都可以赋值给空对象类型,用起来比较复杂,建议不要使用这种方式。

    let car: {}
    car = {}
    car = { name: 'audi' }
    car = []
    car = 'ToYoTa'
    

    Object

    最后一种声明对象类型的方式:Object。和{}的作用一样的。不推荐使用。

    通常情况下,只推荐前两种方式声明对象类型。如果对对象的字段没有要求,那么就使用第一种。如果想知道对象有哪些字段,或者对象的值都为相同的类型,就使用第二种。

    类型字面量

    我们先来看一段代码。

    let show: true = true
    let disable: false = true
    // Error Type 'true' is not assignable to type 'false'.
    

    可以看到show的类型并不是普通的boolean类型,而是只为trueboolean类型。这就是类型字面量。而disabele变量的类型为false,但值确是true。此时,TS就报错了。

    把类型设为某个值,就限制了变量在所有值中只能取指定的值。这称为类型字面量

    类型字面量:表示一个值的类型。

    TS中可以将对象的属性设为可选属性。用法则是在对象的键和:之间加上一个?符号表示该属性是可选的。

    // 可选属性 height
    let person: {
      name: string,
      age: number,
      height?: number,
    person = {
      name: '小美',
      age: 18
    console.log(person)
    // { name: '小美', age: 18 }
    person = {
      name: '图图',
      age: 18,
      height: 175
    console.log(person)
    // { name: '图图', age: 18, height: 175 }
    

    这里我们将height设置成了可选的(类型为number | undefined)。传不传都行。

    当不确定一个对象的类型时或者在未来会对对象添加更多的键时,可以使用索引签名声明对象的类型。索引签名的语法为[key: T]: U,意思是:这个对象里的键类型为T,值则为U类型。

    let person: {
      [key: string]: any
    } = {
      name: '牛爷爷',
      age: 60,
    person.height = 160
    person.weight = 100
    person.sex = '男'
    

    有了索引签名,除了显式声明的键之外,还可以放心的添加更多键。要注意的是,键的类型只能是stringnumber类型。键的名称可以是任何的词。不一定像上面那样用key

    类型别名用于给类型声明一个新名。

    type Height = number
    type Person = {
      name: string,
      height: Height
    let person: Person = {
      name: '图爸爸',
      height: 180
    

    类型别名和letconst变量声明一样,同一类型不能声明两次。并且也是采用块级作用域。每个代码块和每个函数都有自己的作用域,内部的类型别名会覆盖外部的类型别名。

    type Name = '图图'
    let n = Math.random() < 0.5
    if (n) {
      type Name = '小美' // 覆盖上面声明的Name
      let name: Name = '小美'
      console.log('name=', name)
    } else {
      let name: Name = '图图'
      console.log('name=', name)
    

    交叉类型就是把多个类型合并在一起。并且具备多个类型的特性。也可以叫做并集类型

    type Bad = { name: string, isBad: boolean }
    type Good = { name: string, isGood: boolean, clever: boolean }
    type BadAndGood = Bad & Good
    let person: BadAndGood = {
      name: '蟑螂恶霸',
      isBad: true,
      isGood: false,
      clever: false
    

    上面的代码中,声明了两个不同的类型BadGood。然后使用&运算符声明了BadGood两者之和的交叉类型BadAndGood。该类型具备BadGood这两种类型的属性。

    当一个变量存在不同的类型时,联合类型就派上用场了。

    let height: number | string = 175
    height = '180'
    height = 190
    

    height的值的类型可以是string类型,也可以是number类型。

    JSTS函数声明方式一共有五种。

    // 具名函数
    function Person1(name: string): string {
      return `hello ${name}`
    // 函数表达式
    let Person2 = function (name: string): string {
      return `hello ${name}`
    // 箭头函数表达式
    let person3 = (name: string): string => {
      return `hello ${name}`
    // 箭头函数表达式简写
    let person4 = (name: string): string => `hello ${name}`
    // 函数构造方法
    let person5 = new Function('name', 'return `hello ${name}`')
    

    形参和实参

    我们来简单回顾一下形参实参

  • 形参:声明函数时指定的运行函数所需的数据
  • 实参:调用函数时传给函数的数据
  • 可选参数和默认参数

    TS的函数也是可以用?将参数设为可选的。定义参数时最好是把必要的参数放在前,可选放在后。

    function person(name: string, age?: number) {
      return `大家好,我是${name}。今年${age || 18}岁`
    console.log(person('图图', 20))
    console.log(person('小美'))
    

    JS中的函数参数默认值,在TS中一样支持的。把上面的函数改一下。

    function person(name: string, age: number = 18) {
      return `大家好,我是${name}。今年${age || 18}岁`
    console.log(person('图图', 20))
    console.log(person('小美'))
    

    这个例子中,我们把可选参数age改成了默认值。调用函数时,可以不传。

    TS中的剩余参数和ES6中的剩余参数是一样的。以三点运算符(...)表示。

    function sum(...nums: number[]): number {
      return nums.reduce((total, n) => total + n, 0)
    console.log(sum(1, 2, 3)) // 6
    

    注解this的类型

    TS中,如果函数用到this,就要在函数的第一个参数中声明this的类型(放在其他参数之前),这样每次调用函数时,确保this是你想要的类型。

    function getDate(this: Date) {
      return `${this.getFullYear()}-${this.getMonth()}-${this.getDate()}`
    let day = getDate.call(new Date)
    console.log(day)
    // 2022-4-14
    

    想了解更多关于TS中的this,可以到TS的官方文档查看。

    在学习函数签名之前,先来给一个例子大家看:

    function sum(a: number, b: number): number {
      return a * b;
    

    这个例子中的sum是一个函数,它的类型是Function。但有时候Function类型并不是我们想要的。它并不能体现出函数的具体类型。

    那么,sum函数的类型要怎么表示呢?sum是一个接受两个number参数并返回一个number的函数。在TS中可以像下面这样来表示该函数的类型:

    (a: number, b: number) => number
    

    这个句法在TS表示函数的类型,也叫函数签名(或叫类型签名)。它跟箭头函数非常相似。

    函数签名只包含类型层面的代码。也就是说,只有类型没有值。因此,函数签名可以表示参数的类型、this的类型、返回值的类型、剩余参数的类型和可选参数的类型。但无法表示默认值,因为默认值是值,而不是类型。函数签名没有函数的定义体,无法推导出返回类型,所以必须显式注解。

    下面我们来看看函数签名的使用。

    type Sum = (a: number, b: number) => number
    let sum: Sum = (a, b = 30) => {
      return a + b
    console.log(sum(10, 20))
    

    这个例子中,声明一个函数表达式sum,并注解它的类型为Sum。参数的类型不用再次注解,因为在定义Sum类型时已经注解过了。给b设置一个默认值。类型则从Sum函数签名中推导出,但默认值是不知道的,因为Sum是类型,不包含值。最后不许再次注解返回类型,在Sum函数签名中已经声明为number

    类型层面和值层面代码

    类型层面的代码指的是只有类型和类型运算符的代码。其他都是值层面代码。看下面的例子。

    // 函数的参数、返回值类型、并集类型运算符|都是类型层面
    function add(num: number): number | null {
      if (num < 0) {
        return null;
      num++;
      return num;
    let num: number = 1; // 类型层面
    let total = add(num);
    if (total !== null) {
      console.log(total);
    

    这个例子中,函数的参数、返回值类型、联合类型运算符|都是类型层面。

    函数签名两种句法

    函数签名句法有两种。上面用是简写型函数签名。还有一种是完整行函数签名

    // 简写型函数签名
    type Sum = (a: number, b: number): number
    // 完整型函数签名
    type Sum = {
      (a: number, b: number): number,
    

    这两种写法完全相同,只是使用句法不同。对于比较复杂的函数时,推荐用完整型函数签名句法。也就是下面讲到的函数重载

    函数重载:具有多个函数签名的函数。

    我们都知道在JS中是没有函数重载的,但在TS中有。下面我们通过函数签名来实现一个以不同的方式调用的函数。

    type Sum = {
      (a: number, b: number): number,
      (a: number, b: number, c:number): number
    let sum: Sum = (a:number, b:number, c?:number) => {
      if (c !== undefined) {
        return a + b +c
      return a + b
    console.log(sum(2, 3)) // 5
    console.log(sum(2, 3, 5)) // 10
    

    这个例子中,我们写两个函数签名:一个接受两个参数,另一个接受三个参数。根据传入的参数不同,函数体内所做的事情就不同。

    声明函数重载还有另一种方式,我们来改造一下上面的例子。

    function Sum(a: number, b: number): number
    function Sum(a: number, b: number, c: number): number
    function Sum(a: number, b: number, c?: number) {
      if (c !== undefined) {
        return a + b + c
      return a + b
    

    在类型层面施加约束的占位类型,也叫多态类型参数

    了解泛型之前,我们先来看个例子。

    function merge(arr1: string[], arr2: string[]): string[];
    function merge(arr1: number[], arr2: number[]): number[];
    function merge(arr1: object[], arr2: object[]): object[];
    function merge(arr1: any, arr2: any) {
      return arr1.concat(arr2)
    const strings = merge(['a', 'b'], ['c', 'd', 'e'])
    const numbers = merge([1, 2, 3], [4, 5])
    const objs = merge([{ name: '图图' }], [{ name: '小美' }])
    console.log(strings[0])
    console.log(numbers[0])
    console.log(objs[0].name)
    // Error Property 'name' does not exist on type 'object'.
    

    这个例子中,简单的使用函数重载实现了一个合并字符串数组、数字数组、对象数组的merge函数。访问stringsnumbers数组的第一个元素都没问题。但是当访问objs变量第一个元素的属性时,TS抛出了错误。是因为object无法描述对象的结构,所以抛出了错误。而且没有指明对象的具体结构。

    为了解决这种问题,我们就要用到泛型了。把上面的函数接受的参数改写为泛型。如下:

    function merge<T>(arr1: T[], arr2: T[]): T[];
    function merge<T>(arr1: T[], arr2: T[]) {
      return arr1.concat(arr2)
    const strings = merge(['a', 'b'], ['c', 'd', 'e'])
    const numbers = merge([1, 2, 3], [4, 5])
    const objs = merge([{ name: '图图' }], [{ name: '小美' }])
    console.log(strings[0]) // a
    console.log(numbers[0]) // 1
    console.log(objs[0].name) // 图图
    

    上面代码中,merge函数使用一个泛型参数T,但我们并不知道具体类型是什么。TS从传入的arr1arr2中推导T的类型。调用merge函数时,TS推导出T的具体类型之后,会把T出现的每个地方都替换成推导出的类型。T就像是一个占位类型,类型检查器会根据上下文填充具体的类型。

    泛型使用尖括号<>来声明(你可以把尖括号理解成type关键字,只不过声明的是泛型)。尖括号的位置限定泛型的作用域(只有少数几个地方可以用尖括号),TS将确保当前作用域中相同的泛型参数最终都绑定同一个具体类型。鉴于上面的例子中括号的位置,TS将在调用merge函数时为泛型T绑定具体类型。而为T绑定哪一个具体类型,就取决于调用merge函数时传入的参数。

    T是一个类型名称,也可以使用任何名称,比如NamePersonValue等。

    泛型还可以是多个,在尖括号里以逗号分隔开。

    function getPerson<T, U>(name: T, age: U) {
      return {
        name, age
    const person = getPerson("图图", 18);
    console.log(person) // { name: '图图', age: 18 }
    

    上面代码中,有两个泛型:表示人名的T和年龄的U,最后返回一个具备这两个值的对象。

    声明泛型的位置不仅限制了泛型的作用域。还决定TS什么时候给泛型绑定具体类型。

    type Person = {
      <T, U>(name: T, age: U): {}
    let person: Person = (x, y) => {
      return {
    

    <T, U>在函数签名中声明,TS会在调用Person类型的函数时为TU绑定具体的类型。

    如果把<T, U>的作用域限制在函数签名Person中,TS会要求在使用Perosn时显示绑定类型。

    type Person<T, U> = {
      (name: T, age: U): {}
    // 错误例子
    let person: Person = (name, age) => {
      return { name, age }
    // Error Generic type 'Person' requires 2 type argument(s)
    type OtherPerson = Person
    // Error Generic type 'Person' requires 2 type argument(s)
    // 正确例子
    let person: Person<string, number> = (name, age) => {
      return { name, age }
    type OtherPerson = Person<string, number>
    

    TS在使用泛型时会给泛型绑定具体类型,对于函数来说,在调用函数时。对于类,在实例化时。对于函数签名和接口,在使用别名和实现接口时。

    以上的所有泛型例子,都是让TS自动推导出泛型。不过,也可以显示注解泛型。在显式注解泛型时。要么把所有的泛型都加上,要么都不注解。

    function Person<T, U>(name: T, age: U): {} {
      return { name, age };
    console.log(Person('图图', 23)) // ok
    console.log(Person<string, number>('小美', 18)) // ok
    console.log(Person<string>('牛爷爷', 60))
    // Error Expected 2 type arguments, but got 1
    console.log(Person<string, string>('图爸爸', 49))
    // Error Argument of type 'number' is not assignable to parameter of type 'string'
    

    type关键字可以给类型起新名字,泛型同样也可以。下面来定义一个Event类型,用于描述DOM事件。

    // 类型别名只有在这个地方可以声明泛型
    type DomEvent<T> = {
      target: T,
      type: string
    // 泛型别名
    type DivEvent = DomEvent<HTMLDivElement | null>;
    // 第一种
    let myDiv: DivEvent = {
      target: document.querySelector('#my-div'),
      type: 'click'
    // 第二种 显示注解类型参数
    let myDiv: DomEvent<HTMLDivElement | null> = {
      target: document.querySelector('#my-div'),
      type: 'click',
    

    DomEventtarget属性指向触发事件的元素,比如一个div或者button等。要注意的是,在使用DomEvent泛型时。必须显式注解类型参数,因为TS无法推导。能用泛型的地方,泛型别名一样生效。

    泛型默认类型

    函数的参数可以指定默认值,泛型参数也可以指定默认类型。

    type DomEvent<T = HTMLElement> = {
      target: T,
      type: string
    let myButton: DomEvent<HTMLButtonElement | null> = {
      target: document.querySelector('#my-btn'),
      type: 'click'
    

    注意,泛型默认类型和函数可选参数一样的,有默认类型的泛型要放在没有默认类型的泛型后面。

    // Error Required type parameters may not follow optional type parameters
    type MyEvent<T = HTMLElement, Type> = {
      target: T,
      type: Type
    let myDiv: MyEvent<HTMLElement | null, string> = {
      target: document.querySelector('#my-div'),
      type: 'scroll'
    

    下面我们来看一个类的例子。

    class Fruit {
      fruitName: string
      constructor(name: string) {
        this.fruitName = name
      getFruitName() {
        return this.fruitName
    let person = new Fruit('苹果');
    

    这里声明了一个Fruit类。该类有三个成员:一个fruitName属性、一个构造函数和一个getFruitName方法。引用类中的成员时用了this,它表示我们访问类中的成员。

    最后,使用new运算符创建了Fruit类的实例,它会调用之前给定的构造函数,创建一个Fruit类型的新对象,并执行构造函数初始化它。

    和JS一样,通过extends关键字实现子类继承父类的属性和方法。

    class Fruit {
      fruitName: string
      constructor(name: string) {
        this.fruitName = name
      getFruitName() {
        return this.fruitName
    class Grape extends Fruit {
      fruitName: string
      price: number
      constructor(name: string, price: number) {
        super(name);
        this.fruitName = name
        this.price = price
    const grape = new Grape('葡萄', 15)
    console.log(grape.getFruitName()) // 葡萄
    

    这个例子中,Grape子类继承了Fruit父类上的属性和方法。在创建Grape的实例后,通过子类调用父类中的方法。

    注意,子类有一个构造函数。在构造函数中必须调用super(),把父子关系连接起来。并且要在构造函数访问this的属性之前调用。

    类中的修饰符

    类中的属性和方法支持以下三个访问修饰符:

  • public公有的,任何地方都可以访问
  • protected受保护的,只能在当前类及其子类中访问
  • private私有的,只能在当前类访问
  • public

    在TS中,类的成员默认为公有的。你也可以把标记成员为public

    class Fruit {
      public fruitName: string
      constructor(name: string) {
        this.fruitName = name
      public getFruitName() {
        return this.fruitName
    const fruit = new Fruit('西瓜')
    console.log(fruit.getFruitName()) // 西瓜
    

    private

    如果把成员标记为private后,它只能在类中访问。

    class Fruit {
      public fruitName: string
      private price: number
      constructor(name: string, price: number) {
        this.fruitName = name
        this.price = price
      public getFruitName() {
        return this.fruitName
    const fruit = new Fruit('猕猴桃', 50)
    console.log(fruit.price)
    // Error Property 'price' is private and only accessible within class 'Fruit'
    

    protected

    把成员标记为protected后,在子类中可以访问,但不能在父类或子类外访问。

    class Fruit {
      public fruitName: string
      protected price: number
      constructor(name: string, price: number) {
        this.fruitName = name
        this.price = price
      public getFruitName() {
        return this.fruitName
    class Watermelon extends Fruit {
      constructor(name: string, price: number) {
        super(name, price);
        this.fruitName = name
        this.price = price
      public getPrice() {
        return this.price
    const watermelon = new Watermelon('西瓜', 3)
    console.log(watermelon.getPrice()) // 3
    console.log(watermelon.price)
    // Error Property 'price' is protected and only accessible within class 'Fruit' and its subclasses
    

    readonly

    声明实例属性时可以使用readonly修饰符把属性标记为只读。readonly修饰符不只是可以在类中使用,还可以把数组和对象属性设为只读。

    class Fruit {
      readonly fruitName: string
      readonly price: number
      constructor(name: string, price: number) {
        this.fruitName = name
        this.price = price
    const fruit = new Fruit('火龙果', 10)
    fruit.price = 20
    fruit.fruitName = '荔枝'
    // Cannot assign to 'fruitName' because it is a read-only property
    

    抽象类是作为其它子类的父类使用,它不能被实例化。抽象类可以包含成员的实现细节。使用abstract关键字来定义抽象类、抽象方法。

    abstract class Fruit {
      fruitName: string
      price: number
      constructor(name: string, price: number) {
        this.fruitName = name
        this.price = price
      abstract getPrice(): number
      abstract getName(): string
    const fruit = new Fruit('龙眼', 15)
    // Error Cannot create an instance of an abstract class
    

    这个例子中,用abstract关键字定义了Fruit类,直接实例化Fruit后,TS直接报错了。

    下面我们通过子类去实现抽象类中定义的方法。

    abstract class Fruit {
      fruitName: string
      price: number
      constructor(name: string, price: number) {
        this.fruitName = name
        this.price = price
      abstract getPrice(): number
      abstract getName(): string
    class Watermelon extends Fruit {
      constructor(name: string, price: number) {
        super(name, price)
        this.fruitName = name
        this.price = price
      getPrice(): number {
        return this.price
      getName(): string {
        return this.fruitName
    const watermelon = new Watermelon('西瓜', 6)
    console.log(watermelon.getPrice()) // 6
    console.log(watermelon.getName()) // 西瓜
    

    这里的Watermelon类继承了Fruit抽象类,并且实现了Fruit类上定义的getPricegetName方法。如果忘记实现其中一个方法,TS就会抛出错误。

    抽象类中的抽象方法是不会具体实现的,而是交给子类实现。抽象类必须要有一个抽象方法,继承抽象类的子类必须重写抽象方法。也就是说抽象类负责定义,子类负责实现。

    和类型别名类似,接口是一种命名类型的方式。类型别名和接口算得上是同一个概念的两种句法,就跟函数表达式和函数声明之间的关系。但两者之间还是会存在一些差别的。先来看看二者的共同点:

    type Fruits = {
      name: string,
      price: number,
      feature: string,
      weight: number,
    // 使用接口重新定义上面的类型别名
    interface Fruits {
      name: string,
      price: number,
      feature: string,
      weight: number,
    

    在使用Fruits类型别名的地方都能用Fruits接口。两者都是定义结构。

    还可以把类型组合在一起。

    type Fruits = {
      price: number,
    type Watermelon = Fruits & {
      feature: string,
    type Banana = Fruits & {
      weight: string,
    // 使用接口重新定义上面的类型别名
    interface Fruits {
      price: number,
    interface Watermelon extends Fruits {
      feature: string,
    interface Banana extends Fruits {
      weight: string
    

    接口不一定扩展其他接口。接口可以扩展任何结构:对象类型、类或其他接口。

    接口和类型别名的差异

    类型别名和接口之间有三种差别。

  • 类型别名更通用,右边可以是任何类型,包括类型表达式(类型外加&|运算符);而在接口声明中,右边只能是结构。看下面的例子。
  • type Str = string
    type StrAndNum = Str | number
    
  • 扩展接口时,TS会检查扩展的接口是否可赋值给被扩展接口
  • interface Person {
      good(x: number): string,
      bad(x: number): string,
    interface BadPerson extends Person {
      good(x: number | string): number
      bad(x: string): string // Error
    // 替换成类型别名,不会抛出错误
    type Person = {
      good(x: number): string,
      bad(x: number): string,
    type BadPerson = Person & {
      good(x: number | string): number
      bad(x: string): string
    

    这个例子中,将接口换成类型别名,把extends换成交集运算符&,TS会把扩展和被扩展的类型组合在一起,最后的结果就是重载bad的签名。

  • 同一个作用域中的多个同名接口会自动合并;同一个作用域中的多个类型别名会导致编译时报错。这个特性叫做声明合并
  • implements(实现)关键字的作用是指明该类满足某个接口。和其他显示类型注解一样。这是给类添加类型层面约束的一种方式。这样做的目的是尽可能的保证类在实现上的正确性。看下面的例子。

    interface Fruit {
      getName(): string
      setName(name: string): void
      setPrice(price: number): void
    class Banana implements Fruit {
      name: string
      price: number
      constructor(name: string, price: number) {
        this.name = name
        this.price = price
      getName(): string {
        return this.name
      setName(name: string): void {
        this.name = name
      setPrice(price: number): void {
        this.price = price
    const fruit = new Banana('香蕉', 5)
    console.log(fruit.getName()) // 香蕉
    fruit.setPrice(10)
    console.log(fruit.getPrice()) // 10
    

    这个例子中,Banana类必须实现Fruit接口声明的所有方法。如果有需要,还可以在此基础上实现其他方法和属性。接口还可以声明实例属性,但不能带有可见性修饰符(privatepublic等等),也不能使用static关键字。但是可以使用readonly将实例属性标记为只读。

    interface Fruit {
      readonly name: string
      getName(): string
      setName(name: string): void
      setPrice(price: number): void
    

    一个类不限于只能实现一个接口,你想实现多少个都行。

    interface Fruit {
      name: string
      price: number
      getName(): string
    interface FruitFeature {
      getPrice(): number
    class Banana implements Fruit, FruitFeature {
      name: string
      price: number
      constructor(name: string, price: number) {
        this.name = name
        this.price = price
      getName(): string {
        return this.name
      getPrice(): number {
        return this.price
    const fruit = new Banana('香蕉', 6)
    console.log(fruit.getName()) // 香蕉
    console.log(fruit.getPrice()) // 6
    

    如果忘记实现某个方法或属性,或者实现方式有问题,TS将会抛出错误。

    类是结构化类型

    和其他类型一样,TS会根据结构比较类,和类的名称没有关系。类跟其他类型是否兼容,要看结构。如果一个对象定义了同样的属性或者方法,也和类兼容。

    class Person {
      name: string
      constructor(name: string) {
        this.name = name
      getName(): string {
        return this.name
    class OtherPerson {
      name: string
      constructor(name: string) {
        this.name = name
      getName(): string {
        return this.name
    function getPersonInfo(person: Person) {
      return person.getName()
    let person = new Person('图图')
    let otherPerson = new OtherPerson('小美')
    getPersonInfo(person) // ok
    getPersonInfo(otherPerson) // ok
    

    上面代码中,getPersonInfo函数接收一个person实例。当我们传入person实例和otherPerson实例时。TS并没有报错。在person函数看来,这两个类是可互用的,这两个类都实现了getName方法。如果把方法用到privateprotected修饰符,情况就不一样了。

    TypeScript采用的是结构化类型。对类来说,和一个类结构相同的类型是可以互相赋值的。

    类型断言用作于手动指定一个值的类型。有两种句法。as句法和尖括号<>句法。下面来展示这两种句法的用法。

    interface Fruit {
      name: string,
      price: number
    let fruit: Fruit = {
      name: '猕猴桃',
      price: 30
    function getFruitPrice(price: number): string | number {
      return price
    getFruitPrice(fruit.price as number)
    

    上面的例子中,用类型断言as告诉TS,price是个数字,而不是string | number类型。

    在TypeScript中,通过id获取一个DOM节点,对该节点做某些操作时,通常都要判断该节点是否存在。尽管我们知道一定存在这个元素。但TypeScript只知道该节点的类型为Node | null,所以得用if语句判断。在不确定是否为null的情况下,确实得这么做,但确定不可能是null | undefined时,可以使用TypeScript的非空断言

    // 第一种
    const dom = document.getElementById('item1')!;
    dom.style.display = 'none';
    // 第二种
    const dom = document.getElementById('item1');
    dom!.style.display = 'none';
    

    使用非空断言运算符!,告诉TypeScript,这个变量不可能为null | undefined

    明确赋值断言

    有这么一种情况,假设我们声明了一个变量userName,但还没赋值就想对其做一些处理。实际上TS并不允许这么操作。但可以使用明确赋值断言告诉TS,userName变量一定有值(注意下面的感叹号)。

    // 在没有使用明确赋值断言的情况下
    let userName: string
    userName.slice(0, 3)
    // Error Variable 'userName' is used before being assigned.
    // 使用明确赋值断言后报错消失
    let userName!: string
    userName.slice(0, 3)
    

    const

    TS中有个特殊的const类型,可以禁止变量的类型拓宽。const类型用作类型断言。

    let fruit = {
      name: '苹果',
      price: 10
    } as const
    fruit.price = 15
    // Error Cannot assign to 'price' because it is a read-only property.
    

    const不仅可以限制类型拓宽,还会把成员设置成readonly。不管数据结构嵌套有多深。

    键入运算符

    “键入” 运算符类似JavaScript对象查找字段的句法。用于查找对象属性的类型。用 “键入” 查找属性的类型时,只能使用方括号表示法,不能使用点号表示法。看下面的例子。

    type APIResponse = {
      status: string,
      message: string,
      result: {
        userName: string,
        userId: string,
        state: number
    type Result = APIResponse['result']
    Result的类型为
      userName: string;
      userId: string;
      state: number;
    type Status = APIResponse['status']
    // Status的类型为string
    

    keyof

    keyof运算符可以获取对象类型的所有键的类型,并返回一个字符串字面量的联合类型

    type Fruit = {
      name: string
      price: number,
      weight: number
    type FruitKeys = keyof Fruit // name | price | weight
    let price: FruitKeys = 'price' // 通过
    let weight: FruitKeys = 'weight' // 通过
    let fruitName: FruitKeys = 'Name' // Error
    

    有了keyof运算符,搭配 “键入” 运算符可以安全的读取类型。下面我们来实现一个获取对象中指定属性的值函数。

    function getProperty<O extends object, K extends keyof O>(obj: O, key: K): O[K] {
      return obj[key]
    let fruit: Fruit = {
      name: '苹果',
      price: 10,
      weight: 1
    console.log(getProperty(fruit, 'name')) // 通过
    console.log(getProperty(fruit, 'price')) // 通过
    console.log(getProperty(fruit, 'Weight'))
    // Error Argument of type '"Weight"' is not assignable to parameter of type 'keyof Fruit'
    

    这个例子中,getProperty函数接收一个对象obj和一个键keykeyof O是一个字符串字面量的联合类型,表示obj的所有键。K类型是这个联合类型的子类型。比如,obj的类型是{name: string, price: number, weight: number},那么keyof O的类型就是name | price | weight,而K可以是类型'name'、'price'、'name'|'weight'。接下来,O[K]的类型就是在O中查找K得到的具体类型。如果Kname,那就返回一个字符串。如果K'price' | 'weight',就返回number

    Record类型

    Record类型用于定义一个对象的键(key)和值的类型(value)。看下面的例子。

    interface Options {
      baseUrl: string;
      env: string;
      method: string;
    // 第一种用法
    let options: Record<keyof Options, string> = {
      baseUrl: 'http://www.baidu.com',
      env: 'dev',
      method: 'post',
    // 第二种用法
    let options: Record<string, string> = {
      baseUrl: 'http://www.baidu.com',
      env: 'dev',
      method: 'post',
    

    这个例子中,Record有两种用法。

  • 第一种:使用Record类型后,约束了options对象的键必须和Options接口的键一一对应,而值只能是string
  • 第二种:只是要求options对象的键为string类型,并没有特意的限制options对象的键。
  • 映射类型是TS独有的语言特性。下面我们来看个简单的例子。

    type Keys = 'name' | 'age' | 'height'
    type Person = {
      [P in Keys]: number | string
    let person: Person = {
      name: '小美',
      age: 18,
      height: 165
    

    语法和索引签名的语法相同,只不过内部使用了for...in遍历类型。字符串字面量联合类型Keys,包含了要迭代的属性名集合,最后是结果类型number | string

    其实,Record类型也是用映射类型实现的:

    type Record<K exteds keyof any, V> = {
      [P in K]: V
    

    下面来看几个例子,看看使用映射类型都能做哪些事:

    type Result = {
      userId: number
      tags: string[],
      personName: string
    type APIResponse = {
      message: string,
      status: string,
      result: Result
    // 设置所有键都是可选的
    type OptionAPIResponse = {
      [K in keyof APIResponse]?: APIResponse[K]
    // 设置所有值都可为null
    type NullAPIResponse = {
      [K in keyof APIResponse]: APIResponse[K] | null
    // 设置所有键值只读
    type ReadonlyAPIResponse = {
      readonly [K in keyof APIResponse]: APIResponse[K]
    // 设置所有键值都是可写的 // 等价于APIResponse
    type WritableAPIResponse = {
      -readonly [K in keyof APIResponse]: APIResponse[K]
    // 设置所有字段都是必须的 // 等价于APIResponse
    type MustAPIResponse = {
      [K in keyof APIResponse]-?: APIResponse[K]
    

    最后两个类型使用了减号-+号运算符。一般情况下不会直接使用+加号运算符,因为它通常蕴含在其他运算符中。在映射类型中,readonly等价于+readonly?等价于+?+存在的意义只是为了确保整体协调。

    内置映射类型

    Record<Keys, Values>

    上面已经提过Record类型,用于指定对象的键和值类型

    Partial<Object>

    Partial类型用于把对象类型的每个字段都设置为可选的。

    interface Person {
      name: string;
      age: number;
      height: number;
    let person: Partial<Person> = {
      name: '图图',
      height: 180,
    

    Required<Object>

    Required类型用于把对象类型的每个字段都标记为必须的。

    interface Person {
      name?: string;
      age?: number;
      height?: number;
      weight?: number | string;
    let person: Required<Person> = {
      name: '小美',
      age: 18,
      height: 170,
    // 少传了一个weight属性,TS就报错了
    //Error: Property 'weight' is missing in type '{ name: string; age: number; height: number; }' but required in type 'Required<Person>'
    

    Readonly<Object>

    Readonly类型用于把对象类型中的每个字段都设置为只读。

    interface ListItem {
      id: number;
      area: string;
      goodsName: string;
      price: number;
    let goodsInfo: Readonly<ListItem> = {
      id: 1,
      area: '深圳市南山',
      goodsName: '罗技MX3 master3',
      price: 499,
    goodsInfo.price = 899;
    // Error:  Cannot assign to 'price' because it is a read-only property
    

    Pick<O, K>

    Pick类型用于从一个对象类型中,选取指定的属性,并返回一个新定义的类型。

    interface APIResponse {
      message: string,
      status: string,
      result: string[]
    type Result = Pick<APIResponse, 'result'>
    let res1: Result = {
      result: ['1', '2', '3']
    } // ok
    let res2: Result = {
      result: ['1', '2', '3'],
      status: 'C0000'
    // Error: Type '{ result: string[]; status: string; }' is not assignable to type 'Result'.
    // Result 等价 ArrayResult
    interface ArrayResult {
      notes: string[];
    

    Pick类型新建了一个Result类型,和APIResponse建立映射。在使用Result类型时,res2对象中多出了一个status键。TS报错了。因为Result的类型为{ result: string[] }

    条件类型是TypeScript中比较独特的特性,语法和JavaScript的三元表达式差不多。只不过是用在类型中。一起来看下面的例子。

    type IsString<T> = T extends string ? true : false
    type X = IsString<string> // true
    type Y = IsString<number> // false
    

    这里声明了一个类型IsString,它有一个泛型参数T。条件类型为T extends string,也就是说T是不是为number类型或子类型。如果是,那么得到的类型就为true。否则就是false

    条件类型还可以进行嵌套,而且不限于用在类型别名当中,用到类型的地方几乎都可以用。

    infer关键字

    infer关键字表示在条件类型中待推导的类型。下面来看个例子。

    type ElementType<T> = T extends (infer U)[] ? U : T
    type Y = ElementType<number[]> // number类型
    let y: Y = 1
    // 等价
    type ElementType2<T> = T extends unknown[] ? T[number] : T
    type X = ElementType2<number[]>
    

    在这个例子中,ElementType类型接收泛型参数T。注意,infer子句声明了一个新的类型U,TS会根据传给ElementTypeT推导出U的类型。U是在行内声明的,没有和T一起。

    下面我们再来个复杂一点的例子。

    type Person<T> = T extends { name: infer P; age: infer P } ? P : T
    type Age = Person<{ name: string, age: number }> // string | number
    type Name = Person<{ name: string, age: string }> // string
    let personName: Name = '小美' // ok
    let age: Age = 18 // ok
    let weight: Name = 50 // Error
    

    可以看到,Age的类型为string | numberName类型为string。利用这一特性,可以将元组中的类型转成联合类型。

    内置条件类型

    Exclude<T, U>

    Exclude用于计算在T中而不在U中的类型。

    type X = number | string
    type Name = string
    type Y = Exclude<X, Name> // number
    

    Extract<T, U>

    Extract用于计算T中可赋值给U的类型。

    type X = number | boolean
    type Y = string | boolean
    type Total = Extract<X, Y> // boolean
    

    NonNullable<T>

    NonNullable用于从T中排除nullundefined

    type X = null | string | undefined | boolean
    type Y = NonNullable<X> // string | boolean
    

    ReturnType<F>

    ReturnType用于计算函数的返回类型(不适用于泛型和重载的函数)。

    type CallBack = (p: Record<string, unknown>) => string | null
    type RType = ReturnType<CallBack> // string | null
    

    InstanceType<C>

    InstanceType用于计算类构造方法的实例类型。

    type X = { new(): Y }
    type Y = { name: string }
    type I = InstanceType<X> // { name: string }
    

    在开发的过程中,难以避免出现多个同名但含义不同的函数或者变量等等。虽说不建议这样,但总难以避免这些问题发生。而命名空间就能解决你的这些烦恼。

    假设有这么两个文件:一个文件是封装请求的模块,另一个是使用该模块发起请求。

    // request.ts 这里简单的模拟
    exprot namespace Request {
      export function get(url: string): Promise<unknown> {
        return new Promise((resolve) => {
          resolve(`请求url为${url}`)
    
    // Home.ts
    import { Request } from './request'
    async function getList() {
      const res = await Request.get('http://baidu.com')
      console.log(res)
    getList()
    

    namespace关键词表示命名空间命名空间必须要有名称。可以导出函数、变量、类型、接口或其他命名空间。如果namespace块没有显式导出代码,就表示为所在块的私有代码。

    命名空间还可以导出命名空间,因此命名空间可以进行嵌套。比如Request模块增加了其他的请求方法,分成几个子模块。可以改写成命名空间。

    // request.ts
    export namespace Request {
      export namespace Get {
        export function get(url: string): Promise<unknown> {
          return new Promise((resolve) => {
            resolve(`请求url为${url}`)
      export namespace Post {
        export function post(url: string, data: Record<string, unknown>): Promise<unknown> {
          return new Promise((resolve) => {
            resolve(`请求url为${url}`)
    
    // Home.ts
    import { Request } from './get'
    async function getList() {
      const res = await Request.Get.get('http://baidu.com')
      console.log(res)
    async function saveForm() {
      const data = {
        userName: '小美',
        password: '123456'
      const res = await Request.Post.post('http://baidu.com', data)
    getList()
    saveForm()
    

    现在我们把Request模块中的请求方法分成了几个子命名空间。不过,命名空间和接口一样。可以声明多个同名的命名空间,TS会递归合并名称相同的命名空间。

    export namespace Request {
      export namespace Get {
        export function get(url: string): Promise<unknown> {
          return new Promise((resolve) => {
            resolve(`请求url为${url}`)
    export namespace Request {
      export namespace Post {
        export function post(url: string, data: Record<string, unknown>): Promise<unknown> {
          return new Promise((resolve) => {
            resolve(`请求url为${url}`)
    

    用TypeScript开发的过程中。都会新建一个types文件,里面的文件扩展名为.d.ts。如果类型声明不多的话,在顶级目录建一个types.d.ts的文件。这正是类型声明文件。类型声明的语法和常规的TS代码差不多。不过,还是有一些区别的。

  • 类型声明只含类型,不能有值。这说明,类型声明不能实现函数、类、对象或变量,参数也不能有默认值
  • 类型声明虽然不能定义值,但可以声明JS代码中定义了某个值。不过,得用declare关键字
  • 类型声明只声明使用方可见的类型。如果代码不导出,或者是函数体内的局部变量,则不为其声明类型
  • 类型声明可以做到以下几件事:

  • 告诉TypeScript,JavaScript文件定义了某个全局变量。
  • 定义在项目中用到的类型。
  • 描述通过npm安装的第三方模块。
  • 类型声明按照约定,如果有对应的.js文件,类型声明文件使用.d.ts扩展名;否则,使用.ts扩展名。

    外参变量声明让TypeScript知道全局变量的存在,不用显式导入即可在项目任何.ts.d.ts文件里使用。

    例如,最近在用vue3和TypeScript做项目时,用到了process对象设置axiosbaseURL。编辑器提示报错process没定义,但代码在浏览器上运行也没有报错。经过百度才知道,vue3中已经将process移除了。要在vite.config.ts里面单独定义,并且要在types文件夹下添加一个process类型声明的文件。告诉TS,有一个全局对象process。编辑器的报错提示也就消失了。

    declare const process: {
      env: {
        baseURL: string;
    

    外参变量声明不止是可以声明变量,还可以声明方法declare functiondeclare class等等。

    外参类型声明通常用于定义数据类型。例如,后端返回的数据中有哪些字段,这些字段的类型又是什么。

    // request.d.ts
    export interface Response {
      status: string;
      message: string;
      result: ResponseListResult | ResponseObjectResult;
    interface ResponseListResult {
      items: [];
      pageCount: number;
      currentPage: number;
      recordCount: number;
    interface ResponseObjectResult {
      data: Record<string, never>;
    // index.vue
    async function requestData() {
      try {
        const response: Response = await getList();
      } catch (error) {
        console.log(error);
    

    这样有很大的好处,在输入代码的过程中,支持TypeScript的编辑器(例如VSCode)会智能提示定义的字段。

    在使用一些第三方库时,这些库大多数都是用JS写的,并且也没有声明文件。当引入时,会提示找不到模块的声明文件。有两种解决方案,一是安装该库的声明文件(VSCode会提示你安装)。二是我们自己编写一个该库的声明文件。看下面的例子。

    // index.ts
    import TIM from 'tim-js-sdk'
    Error Could not find a declaration file for module 'tim-js-sdk'. 
    'ts/node_modules/tim-js-sdk/tim-js.js' implicitly has an 'any' type.
    Try `npm i --save-dev @types/tim-js-sdk` if it exists or add a new declaration (.d.ts) 
    file containing `declare module 'tim-js-sdk';`
    

    引入tim-js-sdk后,提示找不到模块tim-js-sdk的声明文件。接下来,我们为它创建一个全局声明文件。

    // index.ts
    import TIM from 'tim-js-sdk'
    // types.d.ts
    declare module 'tim-js-sdk' {
      export default Object
    

    这里只是为了做演示,给tim-js-sdk定义导出一个Object。此时报错就消失了。实际上这个Object类型定不定义都无所谓,直接声明一个模块也是可以的。只是说这个模块的类型是一个any。像下面这样:

    // index.ts
    import TIM from 'tim-js-sdk'
    // types.d.ts
    declare module 'tim-js-sdk'
    

    模块声明支持通配符导入。有了通配符导入,可以给任何导入的路径声明类型。路径使用通配符*匹配即可

    declare module '*.png';
    declare module 'json!*' {
      let value: object;
      export default value;
    declare module '*.css' {
      let css: CSSRuleList;
      export default css;
    

    现在我们就可以引入匹配*.pngjson!**.css文件内容了。

    // 加载.png文件
    import logo from '../assets/logo.png';
    // 加载json文件
    import jsonFile from './json!File';
    // 加载CSS文件
    import cssFile from './index.css';
    cssFile; // CSSRuleList
    

    三斜杠指令

    三斜杠指令以三条斜线///开头,三斜杠指令只能放在包含它的文件最上面。后面跟一个可用的XML标签;各XML标签有一些必须设置的属性。

    types指令

    types指令用于对某个包依赖。

  • 声明依赖@types/node/index.d.ts
  • /// <reference types="node" />
    

    只有在你需要写一个d.ts文件时才用这个指令。

    path指令

    path指令和types指令类似,用于本地声明文件之间的依赖。

    /// <reference path="./types/xxx.d.ts" />
    

    以上是这几个月学习TypeScript过程所做的总结。哪里有不对的地方,请各位大佬多多指点!如果文章对你有所帮助,欢迎点赞加关注哦!

  • 《TypeScript编程》
  • TypeScript官网
  • 图图学编程 前端工程师 @ 深圳某中型公司 16.6k
    粉丝