TypeScript 面试题整理

174 阅读39分钟

为什么推荐使用 TypeScript?

  • 增加了代码的可读性和可维护性
    • 类型系统实际上是最好的文档,大部分的函数看看类型的定义就知道如何使用了。
    • 可以在编译阶段就发现大部分的错误,这总比在运行时出错好。
    • 增强了编译器和IDE的功能,包括代码补全、接口提示、跳转到定义、重构等。
  • 非常包容
    • Typescript是javascript的超集,.js文件可以直接重命名为.ts即可。
    • 即使不显示的定义类型,也能够自动做出类型推导。
    • 可以定义从简单到复杂的几乎一切类型。
    • 兼容第三方库,即使第三方库不是用 TypeScript 写的,也可以编写单独的类型文件供 TypeScript 读取。
  • 面向对象编程支持
    • 支持类、接口、继承等面向对象编程的特性,使得代码更加模块化和可复用。
    • 接口可以定义对象的形状,强制实现特定的方法和属性,提高了代码的规范性。
    • 继承和多态性使得代码更加灵活,可以根据不同的需求进行扩展和定制。
  • 大型项目适用性强:
    • 在大型项目中,TypeScript 的强类型系统和良好的工具支持可以帮助开发者更好地管理复杂的代码结构,提高代码的可维护性和可扩展性。
    • 可以更好地进行代码重构,因为类型系统可以确保在重构过程中不会引入类型错误。
  • 拥有活跃的社区
    • 大部分第三方库都有提供给Typescript的类型定义文件。

使用 TypeScript 遇到的问题?

一些第三方包没有 TypeScript 类型定义:在使用 TypeScript 开发时,如果使用到一些第三方包,需要编写相应的 TypeScript 类型定义文件。但是有时这些依赖包没有相应的类型定义文件,这就需要自己编写,这可能会拖延项目进展,增加开发难度。

TypeScript 的主要特点是什么?

  • 跨平台:TypeScript 编译器可以安装在任何操作系统上,包括 Windows、macOS 和 Linux。
  • ES6 特性:TypeScript 包含计划中的 ECMAScript 2015 (ES6) 的大部分特性,例如箭头函数。
  • 面向对象的语言:TypeScript 提供所有标准的 OOP 功能,如类、接口和模块。
  • 静态类型检查:TypeScript 使用静态类型并帮助在编译时进行类型检查。因此,你可以在编写代码时发现编译时错误,而无需运行脚本。
  • 可选的静态类型:如果你习惯了 JavaScript 的动态类型,TypeScript 还允许可选的静态类型。

命名空间与模块的理解和区别

  • 命名空间 命名空间一个最明确的目的就是解决重名问题。命名空间定义了标识符的可见范围,一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的。可以把它看做一种将相关的代码组织在一起的方式,它通过将代码包裹在一个命名的作用域中来避免全局命名冲突,也是一种逻辑上的分组,用于组织代码并提供一定程度的封装。

    namespace MyNamespace {
      export function someFunction() {
        return "This is a function inside namespace"
      }
    }
    

    可以在单个文件中定义多个命名空间,也可以跨文件扩展命名空间。

  • 模块

    • 模块是一种更现代的代码组织方式,遵循现代 JS 的模块系统规范(如 ES6 模块)。模块通过明确的导入和导出语句来管理代码的依赖关系和可见性。
    • TypeScript 与 ES2015 一样,任何包含顶级 import 或者 export 的文件都被当成一个模块。相反地,如果一个文件不带有顶级的 import 或者 export 声明,那么它的内容被视为全局可见的。
    • 如果一个文件不包含 export 语句,但是希望把它当作一个模块(即内部变量对外不可见),可以在脚本头部添加一行语句export {}
    // moduleA.ts
    export function someFunctionFromModuleA() {
      return "This is a function from module A"
    }
    // moduleB.ts
    import { someFunctionFromModuleA } from './moduleA'
    console.log(someFunctionFromModuleA())
    

    明确的依赖关系管理,使得代码的结构更加清晰,易于维护和测试。

区别:

  • 语法和结构
    • 命名空间使用 namespace 关键字来定义,代码通常在一个大的作用域内组织。而模块使用 import 和 export 关键字来明确地导入和导出特定的函数、类或变量。
    • 模块的语法更加简洁和明确,有助于提高代码的可读性和可维护性。
  • 依赖管理
    • 模块有明确的依赖关系管理机制,通过导入语句可以清晰地看出一个模块依赖哪些其他模块。而命名空间的依赖关系相对不那么明显,可能需要开发者自己去理解和管理。
  • 作用域和可见性
    • 模块中的导出内容具有明确的作用域和可见性控制,可以选择只导出特定的部分,而命名空间中的内容相对更容易被意外访问到。
    • 模块可以更好地控制代码的封装性,避免不必要的全局污染。
  • 与现代 JavaScript 生态的兼容性
    • 模块是现代 JS 的标准组织方式,与各种工具和运行环境的兼容性更好。而命名空间在一些较老的环境中可能更容易被支持,但在与现代工具集成时可能需要额外的配置和处理。

怎么选择命名空间和模块?

  • 命名空间
    • 在一些简单场景下,可以快速地将相关的类型和函数组织在一起,无需复杂的模块导入导出语法。
    • 对于一些简单的工具类或辅助函数,可能不需要复杂的模块结构。可以使用命名空间将它们组织在一起,方便在项目中使用。例如,一些通用的数学函数、字符串处理函数等,可以放在一个命名空间中,避免全局污染。
  • 模块
    • 大型项目通常由多个功能模块组成,每个模块负责特定的业务逻辑或功能。模块可以将这些功能独立封装,使得代码结构清晰,易于维护和扩展。例如,在一个电商项目中,可以有用户管理模块、商品管理模块、订单管理模块等,每个模块都有自己独立的文件,通过明确的导入和导出语句进行交互。
    • 大型项目一般依赖很多外部库和内部模块。模块系统可以有效地管理这些依赖关系,确保代码的稳定性和可维护性。通过模块加载器(如 Webpack),可以自动处理模块之间的依赖关系,实现按需加载和优化打包。
    • 在大型团队中,多个开发人员同时开发不同的模块。模块的独立性使得团队成员可以并行工作,减少代码冲突的可能性。每个模块都可以有明确的职责和接口,方便团队成员之间的沟通和协作。
    • 流行的前端框架(如 Angular、React、Vue.js)都采用模块系统来组织代码。在使用这些框架开发大型项目时,使用模块可以更好地与框架集成,提高开发效率。

.d.ts 类型声明文件

.d.ts文件是 TypeScript 中的类型声明文件,用于声明 TypeScript 项目中使用的类型信息。这些文件扩展名为.d.ts,通常用于以下几种情况:

  • 声明现有 JS 库的类型信息:很多第三方 JS 库并没有附带 TypeScript 类型定义。通过创建.d.ts文件,可以为这些库提供类型声明,使得 TypeScript 能够理解库的结构并提供类型检查和自动完成等功能。
  • 声明全局变量:如果页面上有一些全局变量或函数,可以在.d.ts文件中声明它们,确保 TypeScript 知道这些全局变量的类型。
  • 声明模块:如果你正在使用一些模块,而这些模块没有类型定义,可以创建一个.d.ts文件来声明模块的接口。

在使用.d.ts文件时,需要注意以下几点:

  • .d.ts文件不需要被编译成 JS,它们仅用于类型检查。
  • 通常.d.ts文件放在项目的类型目录中,例如src/types
  • 可以通过 TypeScript 的 typeRoots 或 types 配置选项来指定编译器查找.d.ts文件的位置。

如何为一个没有类型声明的 JS 库创建.d.ts文件?

  1. 在创建类型声明文件之前,你需要对要声明类型的 JavaScript 库有足够的了解。熟悉它的 API、函数签名、对象结构以及使用方法。这将帮助你准确地为其创建类型声明。
  2. 创建一个新的 TypeScript 声明文件,文件名通常与 JS 库的名称相同,以.d.ts为扩展名。在声明文件中,可以使用 declare 关键字来声明变量、函数、类等的类型。
  3. 声明全局变量:如果 JS 库在全局范围内添加了变量,可以使用declare vardeclare letdeclare const来声明这些全局变量的类型。
  4. 声明函数:对于库中的函数,可以使用declare function来声明其类型。指定函数的参数类型和返回值类型。
  5. 声明类和接口:如果库中包含类或接口,可以使用declare classdeclare interface来声明它们的类型。
  6. 模块导入声明:如果 JS 库是一个模块,可以使用 import 和 export 语句来声明其导入和导出的类型。
  7. 测试和完善类型声明:在创建类型声明文件后,进行测试以确保类型声明的准确性。在 TypeScript 项目中使用该 JS 库,并检查是否有类型错误或不明确的地方。根据测试结果,不断完善类型声明文件,直到满足需求。

TS 支持的修饰符有哪些?

可访问:

  • public(公共的)
    • 这是默认的修饰符。被标记为 public 的成员可以在任何地方被访问,包括类的内部、子类以及类的实例对象外部。
    • 使用场景:当一个属性或方法需要在类的外部、子类以及其他任何地方都可以被自由访问和修改时,使用public修饰符。例如,一些通用的配置属性或者公开的接口方法。
  • private(私有的)
    • 被标记为 private 的成员只能在其所属的类内部被访问。在类的外部以及子类中都不能直接访问私有成员。
    • 使用场景:当一个属性或方法只应该在类的内部被使用,不希望被外部直接访问或修改时,使用 private 修饰符。这有助于封装内部实现细节,提高代码的安全性和可维护性。例如,在一个数据存储类中,可能有一个内部的存储结构,不希望外部直接操作这个存储结构,就可以将其设为 private。
  • protected(受保护的)
    • 被标记为 protected 的成员可以在其所属的类内部以及子类中被访问,但不能在类的实例对象外部直接访问。
    • 使用场景:当一个属性或方法需要在类的内部以及子类中被访问,但不希望在类的外部直接访问时,使用 protected 修饰符。这在实现继承关系时非常有用,可以让子类访问父类的某些特定成员。例如,在一个动物类的继承体系中,动物类可能有一个受保护的属性表示健康状态,子类可以根据这个属性进行特定的行为。

不可访问:

  • readonly(只读的)
    • 用于将属性标记为只读,一旦初始化后就不能再被修改。
    • 使用场景:当一个属性在初始化后不应该被修改时,使用readonly修饰符。这可以保证数据的一致性和安全性,防止意外的修改。例如,在一个配置类中,可能有一些只读的配置参数,一旦设置就不应该被改变。

Declare 关键字有什么作用?

declare 关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用。就是让当前文件可以使用其他文件声明的类型。

举例来说,自己的脚本使用外部库定义的函数,编译器会因为不知道外部函数的类型定义而报错,这时就可以在自己的脚本里面使用declare关键字,告诉编译器外部函数的类型。这样的话,编译单个脚本就不会因为使用了外部类型而报错。

declare 关键字可以描述以下类型:

  • 变量(const、let、var 命令声明)
  • type 或者 interface 命令声明的类型
  • class
  • enum
  • 函数(function)
  • 模块(module)
  • 命名空间(namespace)

declare 关键字的重要特点是,它只是通知编译器某个类型是存在的,不用给出具体实现。比如,只描述函数的类型,不给出函数的实现,如果不使用declare,这是做不到的。

declare 只能用来描述已经存在的变量和数据结构,不能用来声明新的变量和数据结构。另外,所有 declare 语句都不会出现在编译后的文件里面。

TS 的数据类型有哪些?

  • 内置
    • Number(数值类型)
    • String(字符串类型)
    • Boolean(布尔类型)
    • Undefined(未定义)
    • Null(空类型)
    • Any(任意类型)
    • Void(空类型)
    • Never(不会出现的类型)
    • Unknown(未知类型)
    • Symbol(唯一且不可变的类型)
  • 用户自定义
    • Enum(枚举类型)
    • Array(数组类型)
    • Tuple(元祖类型)

Enum(枚举)类型

Enum 结构用来将相关常量放在一个容器里面,它既是一种类型,也是一个值,编译后会变成 JavaScript 对象,留在代码中(很大程度上,Enum 结构可以被对象的as const断言替代)。

使用场景:成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。

Enum 成员默认不必赋值,系统会从零开始逐一递增,按照顺序为每个成员赋值,比如0、1、2…… 也可以为 Enum 成员显式赋值:

  • 任意数值,但不能是大整数(Bigint)
  • 成员的值可以相同
  • 成员值都是只读的,不能重新赋值
  • 可以设为字符串,字符串枚举的所有成员值,都必须显式设置

多个同名的 Enum 结构会自动合并,合并时,只允许其中一个的首成员省略初始值,且不能有同名成员。 keyof 运算符可以取出 Enum 结构的所有成员名,作为联合类型返回。 数值 Enum 存在反向映射,即可以通过成员值获得成员名。

Enum 类型使用场景

  • 表示一组固定的常量值:当有一组相关的常量值时,使用 Enum 可以提高代码的可读性和可维护性。例如,一周的天数可以表示为:
enum Days {
  Sunday,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
}
  • 对于需要返回错误代码的功能,Enum 可以提供清晰的错误类型定义:
enum ErrorCode {
  Success,
  NotFound,
  Unauthorized,
  Forbidden,
  InternalError
}
  • 可以用来映射 CSS 类名,确保在 TypeScript 中使用时的类型安全:
enum ClassNames {
  Button,
  PrimaryButton,
  SecondaryButton
}
  • 在权限系统中,Enum 可以定义不同的用户角色或权限级别:
enum Role {
  Guest,
  User,
  Admin,
  SuperAdmin
}
  • 在表单处理中,Enum 可以表示验证的状态:
enum ValidationStatus {
  Valid,
  Invalid,
  Pending
}

any 和 unknown 有什么区别?

any 和 unknown 都是顶级类型,但是 unknown 更加严格,不像 any 那样不做类型检查,反而 unknown 因为未知性质,不允许访问属性,不允许赋值给 any 和 unknown 之外的类型变量。

never 和 void 的区别?

  • void 表示没有任何类型(可以被赋值为 null 和 undefined)。
  • never 表示一个不包含值的类型,即表示永远不存在的值。
  • 拥有 void 返回值类型的函数能正常运行。拥有 never 返回值类型的函数无法正常返回,无法终止,或会抛出异常。

interface 和 type 有什么区别?

interface 和 type 都可以用来定义对象类型,它们的区别在于:

  • 语法不同
    • interface 使用 interface 关键字定义对象类型。
    • type 使用 type 关键字定义对象类型。
  • 扩展方式不同
    • interface 可以使用 extends 关键字进行扩展,可以多重继承。
    • type 可以使用交叉类型&来实现类似的扩展效果。
  • 重复定义行为不同
    • interface 允许重复定义,后面的定义会合并到前面的定义。
    • type 不允许重复定义。
  • 定义类型不同
    • interface 只能定义对象类型的形状,不能直接定义基本类型、联合类型、元组等。
    • type 可以定义各种类型,包括基本类型别名、联合类型、元组类型等。

使用场景:

  • interface 的使用场景
    • 定义对象的形状时,特别是在面向对象编程风格中,用于描述类的结构、接口等。
    • 当需要通过继承来扩展类型时,接口的继承方式更加直观和易于理解。
    • 在与其他开发者协作或使用第三方库时,接口可以作为一种规范,方便其他人理解和扩展代码。
  • type 的使用场景
    • 定义复杂的类型组合,如联合类型、交叉类型、元组类型等,这些在某些情况下使用类型别名会更加简洁。
    • 当需要为基本类型起一个更有意义的名称时,使用类型别名可以提高代码的可读性。
    • 在一些特定的场景下,如函数类型的定义,类型别名可能会更加灵活。

什么是装饰器?有什么优缺点?有哪些使用场景?

装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为:

  • @后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
  • 这个函数接受所修饰对象的一些相关值作为参数。
  • 这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。

装饰器的分类:

  • 类装饰器
  • 属性装饰器
  • 方法装饰器
  • getter 装饰器,setter 装饰器
  • accessor 装饰器
  • 参数装饰器(旧)
  • 存取器装饰器(旧)

装饰器的优缺点:

  • 优点
    • 代码复用性高:装饰器可以应用于多个类、方法或属性,实现了代码的复用。例如,可以创建一个日志装饰器,用于在多个方法执行前后记录日志,而无需在每个方法中重复编写日志记录代码。
    • 增强可维护性:通过将特定的功能(如日志记录、性能监控、数据验证等)封装在装饰器中,可以使核心业务逻辑更加清晰,易于维护。当需要修改这些功能时,只需要在装饰器中进行修改,而不会影响到业务逻辑代码。
    • 灵活性:装饰器可以在不修改原始代码的情况下,动态地为类、方法或属性添加额外的功能。这使得代码更加灵活,可以根据不同的需求在运行时选择是否应用装饰器。
    • 可读性:装饰器可以使代码更加清晰地表达其意图。例如,使用@log装饰器可以一目了然地看出该方法需要进行日志记录。这种自描述性的代码有助于提高代码的可读性和可理解性。
  • 缺点
    • 调试困难:由于装饰器在运行时对代码进行了修改,这可能会使调试过程变得更加复杂。当出现问题时,很难确定是原始代码的问题还是装饰器的问题。此外,调试工具可能无法正确地显示经过装饰器修改后的代码,这也会增加调试的难度。
    • 性能影响:虽然装饰器在大多数情况下对性能的影响很小,但在一些高性能要求的场景下,装饰器的额外开销可能会成为问题。特别是当装饰器执行复杂的操作时,可能会影响程序的性能。
    • 兼容性问题:装饰器是一个相对较新的特性,不同的 JavaScript 和 TypeScript 运行环境对装饰器的支持程度可能不同。这可能会导致在某些环境下无法使用装饰器,或者需要进行额外的配置和兼容性处理。

使用场景:

  • 日志记录:可以使用装饰器在方法执行前后记录日志信息,方便调试和跟踪程序的执行流程
    function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      const originalMethod = descriptor.value
      descriptor.value = function (...args: any[]) {
        console.log(`Calling method ${propertyKey} with arguments ${JSON.stringify(args)}`)
        const result = originalMethod.apply(this, args)
        console.log(`Method ${propertyKey} returned ${result}`)
        return result
      }
      return descriptor
    }
    
    class Calculator {
      @log
      add(a: number, b: number): number {
        return a + b
      }
    }
    const calculator = new Calculator()
    calculator.add(3, 4)
    
  • 性能监控:用于测量方法的执行时间,以分析程序的性能瓶颈
    function measurePerformance(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      const originalMethod = descriptor.value
      descriptor.value = function (...args: any[]) {
        const start = performance.now()
        const result = originalMethod.apply(this, args)
        const end = performance.now()
        console.log(`Method ${propertyKey} took ${end - start} milliseconds.`)
        return result
      }
      return descriptor
    }
    
    class Processor {
      @measurePerformance
      processData(data: any[]): any[] {
        // 一些数据处理逻辑
        return data.map(item => item * 2)
      }
    }
    
  • 数据验证:在方法参数或属性上添加装饰器,用于验证输入数据的合法性
    function validateNumber(target: any, propertyKey: string, parameterIndex: number) {
      const originalMethod = target[propertyKey]
      target[propertyKey] = function (...args: any[]) {
        if (typeof args[parameterIndex]!== 'number') {
          throw new Error(`Argument at index ${parameterIndex} must be a number.`)
        }
        return originalMethod.apply(this, args)
      }
    }
    
    class Validator {
      @validateNumber
      methodWithNumberParam(a: string, @validateNumber b: number): void {
        console.log(`Received parameters: ${a}, ${b}`)
      }
    }
    
  • 权限控制:可以根据用户的角色或权限来决定是否允许执行某个方法
    interface User {
      role: string;
    }
    
    function requireRole(role: string) {
      return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value
        descriptor.value = function (...args: any[]) {
          const user: User = { role: 'user' } // 假设这里获取当前用户
          if (user.role === role) {
            return originalMethod.apply(this, args)
          } else {
            throw new Error(`Access denied. Required role: ${role}`)
          }
        }
        return descriptor
      }
    }
    
    class SecureService {
      @requireRole('admin')
      adminMethod(): void {
        console.log('Admin method executed.')
      }
    }
    

类型断言是什么?

类型断言(Type Assertion)可以用来手动指定一个值的类型。当你比 TS 更了解某个值的类型,并且需要指定更具体的类型时,我们可以使用类型断言。 注意:类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误

语法:值 as 类型<类型>值

<Foo>的语法在 tsx 中表示的是一个 ReactNode,在 ts 中除了表示类型断言之外,也可能是表示一个泛型,所以使用类型断言时,统一使用值 as 类型

用途:

  • 将一个联合类型断言为其中一个类型
  • 将一个父类断言为更加具体的子类
  • 将 any 断言为一个具体的类型

泛型是什么?

泛型提供创建可重用组件的方法的工具。它能够创建可以使用多种数据类型而不是单一数据类型的组件。泛型允许我们创建泛型类,泛型函数,泛型方法和泛型接口。

function identity<T>(arg: T): T {
  return arg
}
// 使用泛型函数
let output1 = identity<string>("myString")
let output2 = identity<number>(100)

工具类型的作用?

  • Partial(部分类型):将类型 T 的所有属性变为可选属性
  • Required (必填类型):将类型 T 的所有属性变为必选属性
  • Readonly (只读类型):将类型 T 的所有属性变为只读属性
  • Exclude (排除类型):从类型 T 中排除可以赋值给类型 U 的部分
  • Extract (提取类型):从类型 T 中提取可以赋值给类型 U 的部分
  • Pick/Omit (排除 key 类型):从类型 T 中排除 K 属性
  • ReturnType (返回值类型):获取函数类型 T 的返回类型

什么是类型守卫?

TypeScript 中的类型守卫是一种类型收缩的机制,它可以缩小变量的类型范围,从而在代码中更安全地使用该变量。TypeScript 中有以下几种类型守卫:

  • 类型谓词函数:
    • 语法:function isFish(pet: Fish | Bird): pet is Fish { ... }
    • 作用:在函数返回值中声明一个类型谓词,TypeScript 会根据此谓词来缩小变量的类型范围。
  • in 操作符:
    • 语法:if ('swim' in pet) { ... }
    • 作用:用于判断对象是否含有某个属性,从而缩小变量的类型范围。
  • instanceof 操作符:
    • 语法:if (x instanceof Animal) { ... }
    • 作用:用于判断变量是否是某个类的实例,从而缩小变量的类型范围。
  • 字面量类型守卫:
    • 语法:if (typeof x === 'string') { ... }
    • 作用:用于判断变量的类型是否为特定的字面量类型,从而缩小变量的类型范围。
  • 受保护的属性访问:
    • 语法:(x as Fish).swim()
    • 作用:用于在不知道变量确切类型的情况下,安全地访问变量的属性或方法。

在处理联合类型时,类型守卫可以带来以下几个优势:

  • 类型安全:通过使用类型守卫,可以在代码中安全地访问联合类型的属性和方法,避免出现运行时错误。
  • 类型缩小:类型守卫可以帮助 TypeScript 编译器缩小变量的类型范围,从而提供更准确的类型推断。
  • 代码可读性:类型守卫可以让代码更加语义化,更容易理解变量的当前类型。如果没有使用类型守卫,就需要使用类型断言或者 instanceof 操作符来手动缩小变量的类型范围,这会使代码更加冗长和不太直观。而使用类型守卫,可以让代码更加简洁、可读性更好,同时也能提供更好的类型安全保证。

tsconfig.json

tsconfig.json是 TypeScript 项目的配置文件,放在项目的根目录。反过来说,如果一个目录里面有tsconfig.json,TypeScript 就认为这是项目的根目录。

如果项目源码是 JavaScript,但是想用 TypeScript 处理,那么配置文件的名字是jsconfig.json,它跟tsconfig的写法是一样的。

tsconfig.json文件主要供tsc编译器使用,它的命令行参数--project或-p可以指定tsconfig.json的位置(目录或文件皆可);如果不指定配置文件的位置,tsc就会在当前目录下搜索tsconfig.json文件,如果不存在,就到上一级目录搜索,直到找到为止。

tsconfig.json文件的格式,是一个 JSON 对象,文件可以不必手写,使用 tsc 命令的--init参数自动生成,里面会有一些默认配置,也可以使用别人预先写好的 tsconfig.json 文件,npm 的@tsconfig名称空间下面有很多模块。

#安装
$ npm install --save-dev @tsconfig/deno

#引用
{
  "extends": "@tsconfig/deno/tsconfig.json"
}
  • include include属性指定所要编译的文件列表,既支持逐一列出文件,也支持通配符。文件位置相对于当前配置文件而定。

    {
      "include": ["src/**/*", "tests/**/*"]
    }
    // **:任意目录    *:任意文件
    
  • exclude exclude属性是一个数组,必须与include属性一起使用,用来从编译列表中去除指定的文件。它也支持使用与include属性相同的通配符。

    {
      "include": ["**/*"],
      "exclude": ["**/*.spec.ts"]
    }
    
  • extends tsconfig.json可以继承另一个tsconfig.json文件的配置。如果一个项目有多个配置,可以把共同的配置写成tsconfig.base.json,其他的配置文件继承该文件,这样便于维护和修改。 extends属性用来指定所要继承的配置文件。它可以是本地文件。如果extends属性指定的路径不是以./../开头,那么编译器将在node_modules目录下查找指定的配置文件。

    {
      "extends": "../tsconfig.base.json"
    }
    

    extends属性也可以继承已发布的 npm 模块里面的 tsconfig 文件。

    {
      "extends": "@tsconfig/node12/tsconfig.json"
    }
    

    extends指定的tsconfig.json会先加载,然后加载当前的tsconfig.json。如果两者有重名的属性,后者会覆盖前者。

  • files files属性指定编译的文件列表,如果其中有一个文件不存在,就会报错。它是一个数组,排在前面的文件先编译。

    {
      "files": ["a.ts", "b.ts"]
    }
    

    该属性必须逐一列出文件,不支持文件匹配。如果文件较多,建议使用includeexclude属性。

  • references references属性是一个数组,数组成员为对象,适合一个大项目由许多小项目构成的情况,用来设置需要引用的底层项目。path属性,既可以是含有文件tsconfig.json的目录,也可以直接是该文件。

    {
      "references": [
        { "path": "../pkg1" },
        { "path": "../pkg2/tsconfig.json" }
      ]
    }
    

    与此同时,引用的底层项目的tsconfig.json必须启用composite属性。

  • compilerOptions compilerOptions属性用来定制编译行为。这个属性可以省略,这时编译器将使用默认设置。

    {
      "compilerOptions": {
        "incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度
        "tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置
        "diagnostics": true, // 打印诊断信息 
        "target": "ES5", // 目标语言的版本
        "module": "CommonJS", // 生成代码的模板标准
        "outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在AMD模块中,即开启时应设置"module": "AMD",
        "lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
        "allowJS": true, // 允许编译器编译JS,JSX文件
        "checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
        "outDir": "./dist", // 指定输出目录
        "rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构
        "declaration": true, // 生成声明文件,开启后会自动生成声明文件
        "declarationDir": "./file", // 指定生成声明文件存放目录
        "emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件
        "sourceMap": true, // 生成目标文件的sourceMap文件
        "inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中
        "declarationMap": true, // 为声明文件生成sourceMap
        "typeRoots": [], // 声明文件目录,默认时node_modules/@types
        "types": [], // 加载的声明文件包
        "removeComments":true, // 删除注释 
        "noEmit": true, // 不输出文件,即编译后不会生成任何js文件
        "noEmitOnError": true, // 发送错误时不输出任何文件
        "noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用
        "importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
        "downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现
        "strict": true, // 开启所有严格的类型检查
        "jsx": "preserve", // 指定 jsx 格式
        "alwaysStrict": true, // 在代码中注入'use strict'
        "noImplicitAny": true, // 不允许隐式的any类型
        "strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
        "strictFunctionTypes": true, // 不允许函数参数双向协变
        "strictPropertyInitialization": true, // 类的实例属性必须初始化
        "strictBindCallApply": true, // 严格的bind/call/apply检查
        "noImplicitThis": true, // 不允许this有隐式的any类型
        "noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)
        "noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)
        "noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
        "noImplicitReturns": true, //每个分支都会有返回值
        "esModuleInterop": true, // 允许export=导出,由import from 导入
        "allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块
        "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
        "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
        "paths": { // 路径映射,相对于baseUrl
          // 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置
          "jquery": ["node_modules/jquery/dist/jquery.min.js"]
        },
        "rootDirs": ["src","out"], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错
        "listEmittedFiles": true, // 打印输出文件
        "listFiles": true// 打印编译的文件(包括引用的声明文件)
      }
    }
    

什么是类类型接口?

interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。

  • 类可以定义接口没有声明的方法和属性。
  • interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。
  • 类可以实现多个接口(其实是接受多重限制),意味着必须部署所有接口声明的所有属性和方法。
    // 第一种:每个接口之间使用逗号分隔
    class Car implements MotorVehicle, Flyable, Swimmable {
      // ...
    }
    // 第二种:类的继承
    class Car implements MotorVehicle {
    }
    class SecretCar extends Car implements Flyable, Swimmable {
    }
    // 第三种:接口的继承
    interface A {
      a:number;
    }
    interface B extends A {
      b:number;
    }
    
  • TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。

什么是函数重载/方法重载?

函数和对象的方法都可以使用重载。方法重载(Method Overloading)是一种允许同一个函数针对不同类型的参数有不同实现的功能。实现方式如下:

  1. 定义多个签名:首先,你需要定义多个具有相同名称但不同参数列表的方法签名。这些签名描述了函数可以接受的参数类型和返回类型。
  2. 提供实现:然后,你需要提供一个实现函数,它可以处理所有定义的签名。这个实现函数通常使用联合类型或类型参数来处理不同的参数类型。
// 前两行类型声明列举了重载的各种情况
function reverse(str:string):string;
function reverse(arr:any[]):any[];
// 第三行是函数本身的类型声明,它必须与前面已有的重载声明兼容
function reverse(stringOrArray:string|any[]):string|any[] {
  if (typeof stringOrArray === 'string') {
    return stringOrArray.split('').reverse().join('');
  } else {
    return stringOrArray.slice().reverse();
  }
}

TS 中的类是什么,如何定义?有什么特性?

在 TypeScript 中,类是一种用于创建对象的模板。类定义了一组属性和方法,这些属性和方法描述了对象的状态和行为。使用 class 关键字来定义一个类。一个基本的类定义包括以下几个部分:

  • 类名: 类的标识符,通常使用Pascal命名法(每个单词首字母大写)。
  • 属性: 类中的数据成员,用于存储对象的状态。可以在属性声明中指定访问修饰符(public、private 或 protected)。
  • 方法: 类中的函数成员,用于定义对象的行为。可以在方法声明中指定访问修饰符。
  • 构造函数: 这是一种特殊的方法,在创建对象时自动调用,用于初始化对象的状态。

特性:

  • 继承:支持类的继承。子类可以继承父类的属性和方法,并可以重写或扩展它们。
  • 封装:支持通过访问修饰符(public、private 和 protected)来控制属性和方法的可见性和访问级别。
  • 多态:子类可以重写父类的方法,从而实现多态行为。
  • 抽象:在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abstract class)。但抽象类可以被继承,通常包含抽象方法,这些方法必须在子类中实现。

如何在 TS 中实现继承?

继承是一种从另一个类获取一个类的属性和行为的机制。它是面向对象编程的一个重要方面,并且具有从现有类创建新类的能力,继承成员的类称为基类,继承这些成员的类称为派生类。继承可以通过使用 extend 关键字来实现。

如何实现多重继承?

在 TypeScript 中没有真正意义上的多重继承,但可以通过以下方式来模拟多重继承的效果:

  • 使用混入(mixin)
    // 1.创建多个混入函数
    function MixinA() {
      return class {
        methodA() {
          console.log('Method A');
        }
      }
    }
    function MixinB() {
      return class {
        methodB() {
          console.log('Method B');
        }
      }
    }
    
    // 2.创建一个类并应用混入
    class MyClass {
      // 可以添加自己的属性和方法
    }
    const EnhancedClass = MixinB()(MixinA()(MyClass))
    const instance = new EnhancedClass()
    instance.methodA()
    instance.methodB()
    
  • 使用接口和类的组合
    // 1.定义多个接口
    interface IA {
      methodA(): void
    }
    interface IB {
      methodB(): void
    }
    
    // 2.创建实现这些接口的类
    class ClassA implements IA {
      methodA() {
        console.log('Method A')
      }
    }
    class ClassB implements IB {
      methodB() {
        console.log('Method B')
      }
    }
    
    // 3.创建一个新类,组合其他类的实例
    class MyClass {
      private a: IA;
      private b: IB;
      constructor() {
        this.a = new ClassA()
        this.b = new ClassB()
      }
      callMethods() {
        this.a.methodA()
        this.b.methodB()
      }
    }
    

需要注意的是,虽然可以通过这些方式实现类似多重继承的效果,但过度复杂的类结构可能会导致代码难以维护和理解。在设计代码时,应尽量遵循单一职责原则和其他良好的设计模式,以保持代码的简洁和可维护性。

如何使用 TypeScript mixin?

TypeScript 支持使用 Mixin 模式来实现多重继承的功能。Mixin 是一种将多个类的行为"混合"到一个类中的技术。

使用 Mixin 的好处是可以在不修改原有类的情况下,向类中添加新的功能。这种方式可以实现多重继承的功能,而不需要依赖于单一的继承层次结构。

TypeScript mixin 和继承有什么区别?

  • 实现方式
    • 继承:
      • 通过关键字extends实现,一个类只能直接继承自一个父类。
      • 语法相对简单直接,在类定义时就明确了继承关系。
    • mixin:
      • 通常通过函数来实现,将一个或多个类的功能组合到一个目标类中。
      • 需要定义 mixin 函数,这些函数接受一个类作为参数并返回一个新的类,新类包含了传入类的功能以及 mixin 函数添加的功能。
  • 灵活性
    • 继承:
      • 继承关系相对固定,一旦确定了父类,在后续的开发中修改继承关系可能会比较困难,因为可能会影响到整个类层次结构。
      • 对于复杂的继承层次,可能会导致代码的复杂性增加,难以理解和维护。
    • mixin:
      • 更加灵活,可以在运行时动态地将不同的功能组合到一个类中,而不需要预先确定固定的类层次结构。
      • 可以根据需要选择不同的 mixin 组合,方便地为类添加或移除特定的功能。
  • 功能组合
    • 继承:
      • 主要是为了实现代码的复用和层次化的结构设计,子类继承父类的属性和方法。
      • 通常用于表示 “是一种” 的关系,例如 “狗是一种动物”。
    • mixin:
      • 侧重于功能的组合,可以将多个不同来源的功能组合到一个类中,而不一定有严格的层次关系。
      • 适用于将一些独立的、可复用的功能模块添加到不同的类中,而不关心类之间的继承关系。
  • 多重功能添加
    • 继承:
      • 如果需要从多个类继承功能,只能通过多层继承或者使用一些复杂的设计模式来实现,这可能会导致代码的复杂性增加。
      • 多重继承在很多编程语言中是不被支持或者容易引起歧义的。
    • mixin:
      • 可以轻松地将多个 mixin 应用到一个类上,实现多个功能的组合。
      • 每个 mixin 专注于一个特定的功能,使得代码更加清晰和易于维护。

如何选择继承或 mixin?

在 TypeScript 中选择使用继承还是 mixin,可以考虑以下几个方面:

  • 代码结构和关系表达
    • 继承适用场景:
      • 当存在明确的 “是一种”(is-a)关系时,继承是一个自然的选择。例如,“猫是一种动物”,这种情况下使用继承可以清晰地表达类之间的层次关系。
      • 如果希望利用类的层次结构进行多态性编程,即通过父类引用指向子类对象来实现不同的行为,继承也是合适的。
      • 对于相对简单的类层次结构,继承可以使代码更具可读性和可维护性。
    • mixin 适用场景:
      • 当需要组合多个独立的、不相关的功能模块到一个类中时,mixin 非常有用。这些功能模块之间没有严格的层次关系,只是为了给目标类添加特定的行为。
      • 如果希望在不修改现有类层次结构的情况下,为多个不同的类添加相同的功能,mixin 可以实现这种横向的功能扩展。
      • 对于复杂的功能组合,mixin 可以避免深层的继承层次,使代码更加灵活和易于理解。
  • 代码复用和维护性
    • 继承的优势:
      • 继承可以实现代码的垂直复用,子类可以直接继承父类的属性和方法,减少了重复代码的编写。
      • 在维护方面,如果需要修改父类的功能,所有的子类都会自动继承这些修改,只要修改是向后兼容的。
      • 对于具有共同属性和行为的类,继承可以使代码更加简洁和易于管理。
    • mixin 的优势:
      • Mixin 提供了更灵活的代码复用方式,可以在不同的类之间自由组合功能,而不受严格的继承层次限制。
      • 当需要为多个不相关的类添加相同的功能时,使用 mixin 可以避免在多个子类中重复实现相同的代码,提高了代码的可维护性。
      • Mixin 可以更容易地进行功能的插拔,即可以根据需要添加或移除特定的功能模块,而不会影响整个类层次结构。
  • 复杂性和可读性
    • 继承的考虑因素:
      • 过度使用继承可能会导致复杂的类层次结构,增加代码的理解难度。特别是当继承层次过深时,可能会出现包含了过多的功能,难以维护和扩展。
      • 继承可能会导致紧密耦合的代码,因为子类高度依赖于父类的实现。这可能会限制代码的灵活性和可扩展性。
    • mixin 的考虑因素:
      • Mixin 虽然提供了灵活性,但如果使用不当,可能会导致代码的混乱和难以理解。过多的 mixin 组合可能会使代码变得复杂,难以追踪功能的来源。
      • 在使用 mixin 时,需要确保功能模块的独立性和兼容性,以避免出现冲突和意外的行为。

什么是映射文件?

TypeScript 中的映射文件(sourcemap)是一种用于 JavaScript 源代码和 TypeScript 源代码之间转换的映射文件。当 TypeScript 代码编译为 JavaScript 代码时,映射文件会记录下每一行 JavaScript 代码是由哪些 TypeScript 代码生成的。映射文件的主要用途如下:

  • 调试:当在浏览器中调试 JavaScript 代码时,通过使用映射文件,调试器可以将执行点映射回原始的 TypeScript 代码,而不是编译后的 JavaScript 代码。这使得能够更容易地进行调试和问题排查。
  • 错误报告:当运行时出现 JavaScript 错误时,映射文件可以帮助将错误信息映射回原始的 TypeScript 代码位置,从而更容易定位和修复问题。
  • 代码覆盖率:代码覆盖率工具可以使用映射文件将覆盖率信息从 JavaScript 代码映射回 TypeScript 代码,使得能够更好地了解测试覆盖的情况。

通常情况下,使用 TypeScript 编译器(tsc)编译 TypeScript 代码时,会自动生成对应的映射文件。映射文件通常以.map结尾,例如app.js.map。这个文件包含了从 TypeScript 源代码到 JavaScript 输出代码的转换关系。

在开发过程中,可以在tsconfig.json文件中配置是否生成映射文件,以及映射文件的输出位置:

{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist"
  }
}

TS 中的 getter/setter 是什么?如何使用它们?

在 TypeScript 中,getter 和 setter 是一种特殊的属性访问器,用于控制对象属性的读取和设置。getter 允许引用一个值但不能编辑它。setter 允许更改变量的值,但不能查看其当前值。

使用 getter 和 setter 的主要优点包括:

  • 能够在访问属性时执行自定义逻辑,如数据校验、格式化等。
  • 能够控制属性的读写权限,增强数据的安全性。
  • 在不影响外部代码的情况下,可以轻松地修改属性的内部实现。

如何检查 null 和 undefined ?

  • 使用 typeof 运算符:这种方法可以同时检查 undefined 和 null。需要注意的是,typeof null会返回 object,而不是null
    let x: any = null;
    if (typeof x === 'undefined') {
      console.log('x is undefined');
    } else if (x === null) {
      console.log('x is null');
    }
    
  • 使用 === 运算符
    let y: number | null | undefined = 42;
    if (y === null) {
      console.log('y is null');
    } else if (y === undefined) {
      console.log('y is undefined');
    } else {
      console.log('y is a number:', y);
    }
    
  • 使用 typeof 和 === 组合
    let z: any = null;
    if (typeof z !== 'undefined' && z !== null) {
      console.log('z is defined and not null:', z);
    } else {
      console.log('z is undefined or null');
    }
    

const 和 readonly 的区别是什么?

  • const 用于声明常量变量,整个变量都是常量。
  • readonly 用于声明只读属性,属性是常量,但变量本身可以被重新赋值。
  • const 和 readonly 的主要区别在于它们作用的范围不同,const 作用于整个变量,readonly 作用于属性。

在 Vue2 中使用 TypeScript

  • 基本用法
    • 创建 Vue 组件
      import Vue from 'vue'
      
      export default Vue.extend({
        name: 'MyComponent',
        data() {
          return {
            message: 'Hello, TypeScript!'
          }
        },
        methods: {
          sayHello() {
            console.log(this.message)
          }
        }
      })
      
    • .vue文件
      <template>
        <div>{{ message }}</div>
      </template>
      
      <script lang="ts">
      import Vue from 'vue'
      
      export default Vue.extend({
        data() {
          return {
            message: 'Hello from TypeScript in Vue!'
          }
        }
      })
      </script>
      
  • 基于类的 Vue 组件:使用官方维护的vue-class-component装饰器
    import Vue from 'vue'
    import Component from 'vue-class-component'
    
    @Component
    export default class MyComponent extends Vue {
      message: string = 'Hello from component!'
    
      mounted() {
        console.log('Component mounted.')
      }
    }