typescript之前端系列,知其然,知其所以然

TypeScript 已成为现代前端开发不可或缺的一部分,它通过提供静态类型检查,极大地提升了代码的可维护性、可读性和健壮性。在面试中,TypeScript 相关的问题常常用于考察开发者对类型系统、常见特性以及最佳实践的理解。

面试题涉及了 TypeScript 语言的各个方面,包括基本语法类型系统函数模块化泛型装饰器等。在面试中,常见的 TypeScript 面试题主要围绕以下几个方面展开:

类型系统:考察对 TypeScript 类型系统的理解,包括基本类型、联合类型、交叉类型、接口、类型别名、类型推断、类型守卫等。

函数和类:涉及函数参数类型、返回值类型、箭头函数、函数重载、类的定义、继承、访问修饰符等概念。

泛型:考察在函数、类和接口中如何使用泛型来增加代码的灵活性和复用性。

模块化:问题可能涉及 ES6 模块化的语法、导入导出方式以及模块解析等内容。

装饰器:了解对装饰器的使用,包括类装饰器、方法装饰器、属性装饰器以及参数装饰器的定义和应用。

编译配置:熟悉 tsconfig.json 中的配置选项,包括编译目标、模块系统、严格模式等。

工程化实践:了解 TypeScript 在项目中的实际应用,如与 JavaScript 的混用、第三方库的声明文件使用、类型声明等。


TypeScript数据类型

在TypeScript中,常见的数据类型包括以下几种:

  • 基本类型

    • number: 表示数字,包括整数和浮点数。
    • string: 表示文本字符串。
    • boolean: 表示布尔值,即truefalse
    • nullundefined: 分别表示null和undefined。
    • symbol: 表示唯一的、不可变的值。
  • 复合类型

    • array: 表示数组,可以使用number[]Array<number>来声明其中元素的类型。
    • tuple: 表示元组,用于表示固定数量和类型的数组。
    • enum: 表示枚举类型,用于定义具名常量集合。
  • 对象类型

    • object: 表示非原始类型,即除number、string、boolean、symbol、null或undefined之外的类型。
    • interface: 用于描述对象的结构,并且可以重复使用。
  • 函数类型

    • function: 表示函数类型。
    • void: 表示函数没有返回值。
    • any: 表示任意类型。
  • 高级类型

    • union types: 表示一个值可以是几种类型之一。
    • intersection types: 表示一个值同时拥有多种类型的特性。

1. 什么是 TypeScript?它与 JavaScript 有何不同?

概念

  • TS 是 JS 的超集,增加了静态类型系统、接口、枚举等特性。

  • TS 编译为 JS 执行,在编译阶段进行类型检查,能在开发阶段捕获错误,提高可维护性。

TypeScript 是一种由微软开发的开源编程语言 ,它是 JavaScript 的超集 。这意味着任何合法的 JavaScript 代码也都是合法的 TypeScript 代码。TypeScript 最终会被编译 (transpiled) 成纯 JavaScript 代码,才能在浏览器或 Node.js 环境中运行。

与 JavaScript 的主要区别:

  • 静态类型 (Static Typing) :这是最大的区别。TypeScript 在编译阶段 进行类型检查,可以在代码运行前发现潜在的类型错误。JavaScript 是动态类型语言,类型错误只会在运行时暴露。
  • 编译过程:TypeScript 代码需要经过编译才能运行,而 JavaScript 可以直接运行。
  • 更好的工具支持 :由于 TypeScript 具有类型信息,它能为 IDE 提供更强大的代码补全、导航 、重构和错误检查功能。
  • 代码可维护性 :在大型项目中,类型系统使代码更容易理解、维护和协作
  • 面向对象特性增强:TypeScript 支持接口 (Interfaces)、枚举 (Enums)、抽象类 (Abstract Classes) 等更丰富的面向对象特性。

2. interfacetype 有什么区别?何时使用它们?

interfacetype 是 TypeScript 中定义类型的两种主要方式,它们有很多相似之处,但也存在一些关键区别。

主要区别

特性 interface type
用途 定义对象类型或类实现接口 定义任意类型(包括对象、联合、基本类型等)
声明合并 支持,同名的 interface 会自动合并 不支持,同名的 type 会报错
扩展方式 使用 extends 继承 使用 & 交叉类型
实现方式 可以被类实现 (implements) 不能被类直接实现
映射类型 不能直接使用映射类型 可以使用映射类型
可定义内容 属性、函数、索引签名、类的约束 所有类型,包括基本类型别名、对象类型、元组、联合类型、交叉类型、映射类型、条件类型、函数签名等
性能 在大型项目中性能更好 复杂类型可能影响性能

应用场景

interface

  • 使用 interface 的场景:

    • 当你需要定义对象的形状时(尤其是用于公共 API 或库的类型定义)。
    • 当你希望类型可以被多次声明并自动合并(例如,为第三方库添加模块增强时)。
    • 当需要被类实现 (implements) 时。
  1. 定义对象形状(主要用途):

    typescript 复制代码
    interface Person {
      name: string;
      age: number;
    }
  2. 需要声明合并

    typescript 复制代码
    interface User {
      name: string;
    }
    
    interface User {
      age: number;
    }
    
    // 最终 User 包含 name 和 age
  3. 需要被类实现 : "需要被类实现"是指接口可以作为类的设计规范,确保类具有特定的结构和行为,这是 TypeScript 面向对象编程能力的重要体现,也是 interface 区别于 type 的关键特性之一。

    typescript 复制代码
     // 定义一个接口
    interface Vehicle {
      start(): void;
      stop(): void;
      speed: number;
    }
    
    // 类实现接口
    class Car implements Vehicle {
      speed: number = 0;  // 必须实现 speed 属性
    
      start() {           // 必须实现 start 方法
        console.log("Car started");
      }
    
      stop() {            // 必须实现 stop 方法
        console.log("Car stopped");
      }
    }
  4. 与第三方库交互(扩展库的类型定义):

    typescript 复制代码
    // 扩展 window 对象
    interface Window {
      myCustomProp: string;
    }

type

  • 使用 type 的场景:

    • 当你需要定义基本类型别名联合类型交叉类型元组类型等复杂类型组合时。
    • 当你需要定义函数类型时。
    • 当你需要进行类型操作 (如通过 keyoftypeof 等)。
    • 高级类型变换,如 Pick<T, K>、条件类型
    • 定义复杂泛型逻辑
  1. 定义联合类型

    typescript 复制代码
    type Status = 'success' | 'error' | 'pending';
  2. 定义元组类型

    typescript 复制代码
    type Coordinates = [number, number];
  3. 使用映射类型

    typescript 复制代码
    type Readonly<T> = {
      readonly [P in keyof T]: T[P];
    };
  4. 定义复杂类型组合

    typescript 复制代码
    type Tree<T> = {
      value: T;
      left?: Tree<T>;
      right?: Tree<T>;
    }
  5. 需要类型别名简化复杂类型

    typescript 复制代码
    type StringOrNumber = string | number;

最佳实践

对象类型优先用 interface,更复杂的类型组合用 type

在大多数情况下,如果你只是定义对象的形状,使用 interface 更好,因为它在合并和声明方面更直观。对于其他非对象类型的定义,或者需要更灵活的类型组合时,使用 type

interface 是为"面向对象结构"设计的,type 是为"类型变换与表达"设计的。它们不是对立,而是互补。


3. anyunknown 有什么区别?何时使用它们?

anyunknown 都表示不确定的类型,但它们在类型安全性上有显著差异。

  • any (不安全的任意类型)

    • any 表示你可以赋予它任何类型的值,并且可以对 any 类型的值执行任何操作,跳过所有类型检查
    • 使用 any丧失 TypeScript 提供的类型安全性,将代码退化为纯 JavaScript。
    TypeScript 复制代码
    let value: any = 'hello';
    value = 123; // OK
    value.foo(); // OK,编译时不会报错,运行时可能报错
    value[0];    // OK
  • unknown (类型安全的任意类型)

    • unknown 同样可以接受任何类型的值。
    • 但与 any 不同,你不能直接对 unknown 类型的值进行任何操作 (除了赋值给 anyunknown 类型),除非你先通过类型收窄 (Type Narrowing) 来确定它的具体类型。
    • unknown 强制你进行类型检查,从而提供更好的类型安全性。
    TypeScript 复制代码
    let value: unknown = 'hello';
    value = 123; // OK
    
    // value.foo(); // Error: 'value' is of type 'unknown'.
    // value[0];    // Error: 'value' is of type 'unknown'.
    
    if (typeof value === 'string') {
        console.log(value.toUpperCase()); // OK,在 if 块内,value 被收窄为 string
    }

何时使用:

  • 使用 any

    • 临时解决方案:在快速原型开发或处理尚未完全确定的第三方库类型时。
    • 遗留代码集成:处理难以类型化的旧 JavaScript 代码。
    • 避免过度设计:在极少数情况下,当你明确知道某个变量的类型是不确定的,并且类型检查会带来不必要的复杂性时。
  • 使用 unknown

    • 推荐替代 any :当你不知道变量的具体类型,但又希望保留类型安全性时,unknown 是更好的选择。
    • 处理来自外部的数据:例如,从 API 响应中获取的数据,你在使用前需要验证其类型。
    • 编写类型安全的通用工具函数:函数参数可能是任何类型,但你需要在函数内部进行类型判断才能处理。

记住: 优先使用 unknown 而不是 any,因为它会强制你进行类型检查,从而提高代码的健壮性。


4. 类型断言 (Type Assertion)?如何使用?

类型断言 是告诉 TypeScript 编译器你比它更了解某个值的类型。它是一种强制类型转换的方式,但它只在编译时起作用,不会在运行时产生任何转换或检查。

两种语法:

  • 尖括号语法 (Angle-bracket syntax)

    TypeScript 复制代码
    let someValue: any = "this is a string";
    let strLength: number = (<string>someValue).length;
  • as 语法 (As syntax) : 这是在 JSX 中推荐的语法,因为尖括号语法会与 JSX 标签冲突。

    TypeScript 复制代码
    let someValue: any = "this is a string";
    let strLength: number = (someValue as string).length;

何时使用类型断言:

  • 当你明确知道某个值的运行时类型,但 TypeScript 编译器无法自动推断出来时。
  • anyunknown 类型转换为更具体的类型:这是最常见的用法。
  • 处理 DOM 元素 :当你通过 document.getElementById() 获取元素时,你可能需要断言它的具体类型(如 HTMLInputElement)。

使用注意事项:

  • 谨慎使用:类型断言是一种强行绕过类型检查的方式,如果你的断言不正确,它可能导致运行时错误,因为 TypeScript 不会再帮你检查。
  • 避免滥用:过度使用类型断言可能意味着你的类型定义不够精确,或者存在设计问题。

5. 枚举 (Enum)?它有什么作用?

枚举 (Enum) 是 TypeScript 中一个特殊的类型,它允许你定义一组命名的常量。枚举的每个成员都有一个名称和一个对应的数值(默认为从 0 开始递增)。

作用:

  • 提高可读性:使用有意义的名称代替魔术数字或字符串,使代码更易于理解。
  • 增强可维护性:当需要修改某个常量值时,只需在一个地方修改枚举定义,而不是在代码中查找所有出现的地方。
  • 类型安全:限制变量只能使用枚举中定义的成员,避免无效值的出现。

示例:

ts 复制代码
// 数字枚举 (默认)
enum Direction {
    Up,    // 0
    Down,  // 1
    Left,  // 2
    Right  // 3
}

let move: Direction = Direction.Up;
console.log(move); // 0
console.log(Direction.Left); // 2

// 字符串枚举 (值必须手动指定)
enum HttpStatus {
    OK = "OK",
    NOT_FOUND = "NOT_FOUND",
    INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR"
}

let status: HttpStatus = HttpStatus.OK;
console.log(status); // "OK"

// 异构枚举 (不推荐,数字和字符串混合)
enum Mixed {
    No = 0,
    Yes = "YES"
}

枚举的实现原理 (编译后):

  • 数字枚举:会被编译成一个双向映射的对象,既可以从名称查值,也可以从值查名称(反向映射)。

    ts 复制代码
    var Direction;
    (function (Direction) {
        Direction[Direction["Up"] = 0] = "Up";
        Direction[Direction["Down"] = 1] = "Down";
        // ...
    })(Direction || (Direction = {}));
  • 字符串枚举:只会被编译成一个单向映射的对象,只能从名称查值。


6. 联合类型 (Union Types) 和交叉类型 (Intersection Types)?

它们是 TypeScript 中组合现有类型的重要方式。

a. 联合类型 (Union Types) - |

  • 定义 :表示一个值可以是多个类型中的任意一种
  • 操作A | B 表示一个值可以是 A 类型,也可以是 B 类型。
  • 特点 :当使用联合类型时,你只能访问所有联合成员都共享的属性或方法 。如果你想访问特定类型的属性,需要进行类型收窄

示例:

TypeScript 复制代码
let id: string | number; // id 可以是 string 也可以是 number
id = "abc";
id = 123;
// id = true; // Error

function printId(id: string | number) {
    if (typeof id === 'string') {
        console.log(id.toUpperCase()); // 只有在 string 类型时才有的方法
    } else {
        console.log(id.toFixed(2)); // 只有在 number 类型时才有的方法
    }
}

b. 交叉类型 (Intersection Types) - &

  • 定义 :表示一个值必须同时满足多个类型的所有特性。它将多个类型合并为一个新类型,新类型拥有所有原类型的成员。
  • 操作A & B 表示一个值必须同时是 A 类型和 B 类型。
  • 特点 :如果两个类型有同名但类型不兼容的属性,那么交叉后的属性类型会变成 never

示例:

TypeScript 复制代码
interface Point2D {
    x: number;
    y: number;
}

interface Point3D {
    z: number;
}

// 同时拥有 x, y, z 属性
type Point = Point2D & Point3D;

let p: Point = { x: 1, y: 2, z: 3 };

interface HasName {
    name: string;
}

interface HasAge {
    age: number;
}

// 可以定义一个同时拥有 name 和 age 属性的类型
type UserProfile = HasName & HasAge;

let user: UserProfile = { name: "Alice", age: 30 };

7. 泛型 (Generics)?它的作用是什么?

泛型就是在定义函数、接口、类时不预先指定具体类型 ,先使用类型变量表示 ,在使用时再指定具体类型

js 复制代码
function identity<T>(arg: T): T {
  return arg;
}

identity<number>(123);    // => 123
identity<string>('hello'); // => "hello"

这里的 T 是一个类型变量,在调用时由用户传入真实类型。

泛型 是 TypeScript 中一个强大的功能,它允许你编写可重用、灵活且类型安全的代码,使其能够处理多种类型,而不是局限于单一特定类型。

作用

作用 描述
增强复用性 编写一次函数/组件,适配多种类型
保证类型安全 保持输入输出类型一致,避免 any 带来的不确定
增强可读性 any 更明确,更容易阅读和维护
提升开发体验 IDE 自动推导、补全和类型检查

应用场景

1. 泛型函数

TypeScript 复制代码
function identity<T>(arg: T): T {
    return arg;
}

let output1 = identity<string>("myString"); // output1 的类型是 string
let output2 = identity<number>(100);     // output2 的类型是 number
let output3 = identity(true);            // 类型推断为 boolean

2. 泛型接口/类型别名

TypeScript 复制代码
interface Box<T> {
    value: T;
}

let stringBox: Box<string> = { value: "hello" };
let numberBox: Box<number> = { value: 123 };

3. 泛型类

TypeScript 复制代码
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;

    constructor(zeroValue: T, addFunction: (x: T, y: T) => T) {
        this.zeroValue = zeroValue;
        this.add = addFunction;
    }
}

let myGenericNumber = new GenericNumber<number>(0, (x, y) => x + y);
console.log(myGenericNumber.add(10, 20)); // 30

4. 泛型约束(extends)

ts 复制代码
function printLength<T extends { length: number }>(val: T) {
  console.log(val.length);
}

printLength('hello');     // ✅
printLength([1, 2, 3]);    // ✅
printLength(123);         // ❌ number 没有 length 属性

5. 默认泛型参数

ts 复制代码
type ApiResult<T = any> = {
  success: boolean;
  data: T;
};

const res: ApiResult = { success: true, data: 123 }; // 默认 any

总结

泛型在 TypeScript 中的核心作用就是为了实现代码的通用性 ,同时不牺牲类型安全性 。它通过引入类型变量,使得函数、类和接口能够适应不同的数据类型,极大地提高了代码的复用性、灵活性和健壮性,是编写高质量、可维护 TypeScript 代码的关键工具。

泛型 vs any

比较项 泛型 (<T>) any
类型推导 ✅ 自动推导 ❌ 无法推导
类型检查 ✅ 编译报错 ❌ 跳过类型检查
类型安全性 ✅ 安全 ❌ 不安全

8. 映射类型 Partial / Readonly / Pick / Omit 等工具类型的作用?

工具类型 作用
Readonly<T> 将 T 的所有属性变为只读
Partial<T> 将 T 的所有属性变为可选
Required<T> 将 T 的所有属性变为必选
Pick<T, K> 从 T 中挑选一组属性组成新类型
Omit<T, K> 从 T 中排除一组属性组成新类型
Record<K, T> 创建一个类型,其属性名为 K 中的每个成员,属性值为 T 类型

映射类型 是 TypeScript 中一种强大的类型操作工具它允许你基于现有类型创建新类型,通过"映射"现有类型的属性来生成转换后的类型。

基本概念

映射类型就像是一个类型转换函数,它会对现有类型的每个属性进行某种操作,然后生成一个新的类型。其语法形式为:

typescript 复制代码
{ [P in K]: T }

其中:

  • K 是一个属性名的集合(通常是 keyof T
  • P 是集合中的每个属性名
  • T 是属性的类型(可以是原始类型或转换后的类型)

内置映射类型

TypeScript 提供了一些内置的实用映射类型:

1. Readonly<T>

使所有属性变为只读:

typescript 复制代码
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
typescript 复制代码
type ReadonlyPerson = Readonly<{
  name: string;
  age: number;
}>;

// 等同于
type ReadonlyPerson = {
  readonly name: string;
  readonly age: number;
};

2. Partial<T>

使所有属性变为可选:

typescript 复制代码
type Partial<T> = {
    [P in keyof T]?: T[P];
};
typescript 复制代码
type PartialPerson = Partial<{
  name: string;
  age: number;
}>;

// 等同于
type PartialPerson = {
  name?: string;
  age?: number;
};

3. Required<T>

使所有可选属性变为必需(与 Partial 相反):

typescript 复制代码
type Required<T> = {
    [P in keyof T]-?: T[P];
};
typescript 复制代码
type RequiredPerson = Required<{
  name?: string;
  age?: number;
}>;

// 等同于
type RequiredPerson = {
  name: string;
  age: number;
};

4. Pick<T, K>

从类型 T 中选择一组属性 K

typescript 复制代码
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
typescript 复制代码
type Person = {
  name: string;
  age: number;
  address: string;
};

type NameAndAge = Pick<Person, 'name' | 'age'>;

// 等同于
type NameAndAge = {
  name: string;
  age: number;
};

5. Omit<T, K>

ts 复制代码
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
ts 复制代码
interface User {
  id: string;
  username: string;
  password: string;
  email: string;
  createdAt: Date;
}

// 创建不含敏感信息的公开用户类型
type PublicUser = Omit<User, 'password' | 'email'>;

6. Record<K, T>

创建一个类型,其属性名为 K 中的每个成员,属性值为 T 类型:

typescript 复制代码
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
typescript 复制代码
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>;

// 等同于
type ThreeStringProps = {
  prop1: string;
  prop2: string;
  prop3: string;
};

7. Exclude<T, U>

Exclude<T, U> 是 TypeScript 内置的一个实用类型(Utility Type),用于从联合类型 T排除 可以赋值给类型 U 的成员。

typescript 复制代码
type Exclude<T, U> = T extends U ? never : T;
  • T:原始联合类型
  • U:要排除的类型
typescript 复制代码
type T = 'a' | 'b' | 'c' | 'd';

type Result = Exclude<T, 'a' | 'c'>;
// 等价于 'b' | 'd'

9. 类型推断 (Type Inference)?

类型推断 是 TypeScript 的一项核心功能,它指的是 TypeScript 编译器在没有明确指定类型注解的情况下,自动推断变量、函数返回值、表达式等类型的能力。

作用:

  • 简化代码:减少了冗余的类型注解,使代码更简洁。
  • 提高开发效率:你不需要为每个变量都手动添加类型。
  • 保持类型安全:即使没有明确注解,TypeScript 仍然会进行类型检查。

常见推断场景:

  • 变量初始化

    TypeScript 复制代码
    let name = "Alice"; // 推断为 string
    let age = 30;       // 推断为 number
    let isActive = true; // 推断为 boolean
  • 函数返回值

    TypeScript 复制代码
    function add(a: number, b: number) {
        return a + b; // 推断返回类型为 number
    }
  • 数组

    TypeScript 复制代码
    let numbers = [1, 2, 3]; // 推断为 number[]
    let mixed = [1, "hello"]; // 推断为 (number | string)[]
  • 对象字面量

    TypeScript 复制代码
    let user = { id: 1, name: "Bob" }; // 推断为 { id: number; name: string; }

何时会推断为 any

  • 当你声明变量但没有初始化时:

    TypeScript 复制代码
    let value; // 推断为 any
    value = "hello";
    value = 123;
  • 当你明确指定为 any 时。


10. 类型守卫 (Type Guards)?列举几种类型守卫的方式。

类型守卫 是一种 TypeScript 的特性,它允许你在运行时检查变量的类型 ,并根据检查结果,在特定的代码块中将该变量的类型收窄 (Narrowing) 为更具体的类型。这使得你可以在不确定类型的情况下,安全地访问特定类型的属性或方法。

常见类型守卫方式:

a. typeof 类型守卫

用于检查基本数据类型(number, string, boolean, symbol, bigint, undefined, object, function)。

TypeScript 复制代码
function printLength(x: string | number) {
    if (typeof x === 'string') {
        console.log(x.length); // x 在这里被收窄为 string
    } else {
        console.log(x.toString().length); // x 在这里被收窄为 number
    }
}

b. instanceof 类型守卫

用于检查一个值是否是某个类的实例。

TypeScript 复制代码
class Dog {
    bark() { console.log('Woof!'); }
}

class Cat {
    meow() { console.log('Meow!'); }
}

type Animal = Dog | Cat;

function makeSound(animal: Animal) {
    if (animal instanceof Dog) {
        animal.bark(); // animal 在这里被收窄为 Dog
    } else {
        animal.meow(); // animal 在这里被收窄为 Cat
    }
}

c. in 操作符类型守卫

用于检查一个对象是否包含某个属性。

TypeScript 复制代码
interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    layEggs(): void;
}

function isFish(pet: Bird | Fish): pet is Fish { // 类型谓词
    return (pet as Fish).swim !== undefined; // 更推荐使用 in
}

function getSmallPet(pet: Bird | Fish) {
    if ('swim' in pet) {
        pet.swim(); // pet 在这里被收窄为 Fish
    } else {
        pet.fly(); // pet 在这里被收窄为 Bird
    }
}

d. 字面量类型守卫 (Equality Narrowing)

通过比较值是否等于特定的字面量类型(字符串、数字、布尔值)。

TypeScript 复制代码
type Foo = {
    kind: 'foo';
    fooProp: string;
}

type Bar = {
    kind: 'bar';
    barProp: number;
}

function process(item: Foo | Bar) {
    if (item.kind === 'foo') {
        console.log(item.fooProp); // item 被收窄为 Foo
    } else {
        console.log(item.barProp); // item 被收窄为 Bar
    }
}

e. 自定义类型守卫 (User-Defined Type Guards)

通过定义一个返回值为类型谓词 (parameterName is Type) 的函数。

TypeScript 复制代码
interface Car {
    drive(): void;
}

interface Bike {
    ride(): void;
}

// 这是一个自定义类型守卫函数
function isCar(vehicle: Car | Bike): vehicle is Car {
    return (vehicle as Car).drive !== undefined;
}

function startVehicle(vehicle: Car | Bike) {
    if (isCar(vehicle)) {
        vehicle.drive(); // vehicle 在这里被收窄为 Car
    } else {
        vehicle.ride(); // vehicle 被收窄为 Bike
    }
}

11. declare 关键字的作用是什么?

declare 关键字用于告诉 TypeScript 编译器,某个变量、函数、类或模块已经存在于运行时环境中,但它的定义不在当前的 TypeScript 文件中。它主要用于:

  • 声明全局变量或函数 :当你的 TypeScript 代码需要使用在其他地方(如 HTML 文件中的 <script> 标签引入的库,或 Node.js 的全局变量)定义的变量时。
  • 为 JavaScript 库提供类型定义 (Declaration Files - .d.ts) :这是 declare 最主要和最重要的用途。当你使用一个没有 TypeScript 定义的 JavaScript 库时,你可以编写一个 .d.ts 文件来声明该库的模块、函数、类等的类型,这样 TypeScript 编译器就能识别它们,并在你的代码中提供类型检查和智能提示。

示例:

a. 声明全局变量/函数 (在 .ts 文件中,或 .d.ts 文件中)

TypeScript 复制代码
// 假设浏览器环境中已经存在一个全局的 jQuery 对象
declare var $: any;

// 假设全局有一个名为 'myGlobalFunction' 的函数
declare function myGlobalFunction(arg: string): void;

// 在你的代码中就可以直接使用它们,而不会报错
$('div').hide();
myGlobalFunction('hello');

b. 声明模块 (为 JS 库提供类型)

假设你有一个名为 my-js-lib.js 的 JavaScript 库:

TypeScript 复制代码
// my-js-lib.js
module.exports = {
  add: function(a, b) { return a + b; }
};

你可以创建一个 my-js-lib.d.ts 文件来声明它的类型:

TypeScript 复制代码
// my-js-lib.d.ts
declare module 'my-js-lib' {
    function add(a: number, b: number): number;
    // 也可以声明其他导出的成员
    // function subtract(a: number, b: number): number;
    export { add }; // 或者 export default { add }; 根据 JS 模块的导出方式
}

然后你就可以在你的 TypeScript 代码中导入并使用它:

TypeScript 复制代码
import { add } from 'my-js-lib';

let result = add(1, 2); // 编译器知道 add 函数的类型
// let result2 = add(1, '2'); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

declare 关键字本质上是告诉编译器:"相信我,这个东西在运行时会存在并符合我声明的类型,你只要检查我的代码是否正确使用了它就行,不需要你来编译或实现它。"

12. 如何处理 TypeScript 中的第三方库缺少类型定义?

  • 创建声明文件(.d.ts)
  • 使用 @types/[library-name] 安装社区维护的类型定义
  • 使用 declare module '[library-name]' 快速声明

13. 如何配置 tsconfig.json 文件?

  • 重要配置项包括:

    • target: 编译目标 ES 版本 ES6、esnext、CommonJS
    • module: 模块系统
    • strict: 是否启用严格类型检查
    • outDir: 输出目录
    • include/exclude: 包含/排除的文件
js 复制代码
{
  "compilerOptions": {
    "target": "esnext",//1.  -   指定编译后的 JavaScript 目标版本为 `esnext`,即最新的 ECMAScript 标准。
    "module": "esnext",//-   指定模块系统为 `esnext`,即使用 ES 模块(ESM)语法。
    "lib": [//指定 TypeScript 包含的库文件
      "dom",//提供浏览器 DOM 的类型定义
      "dom.iterable",//提供 DOM 可迭代对象的类型定义(如 `NodeList` 的 `forEach`)
      "esnext"//1.  -   -   提供最新的 ECMAScript 特性的类型定义
    ],
    "allowJs": true,//允许编译 JavaScript 文件(`.js` 和 `.jsx`)
    "skipLibCheck": true,//跳过对声明文件(`.d.ts`)的类型检查,可以加快编译速度
    "moduleResolution": "bundler",//指定模块解析策略为 `bundler`,这是 TypeScript 5.0 引入的新选项,适用于现代打包工具(如 Vite、esbuild 等)
    "importHelpers": true,//从 `tslib` 中导入辅助函数(如 `__extends`、`__assign` 等),而不是在每个文件中内联这些函数,可以减少代码体积
    "noEmit": true,//不输出编译后的文件(如 `.js` 文件),仅进行类型检查。通常用于开发环境
    "jsx": "react-jsx",//指定 JSX 语法转换为 `React.createElement` 调用(React 17+ 的新 JSX 转换方式)
    "esModuleInterop": true,//启用 ES 模块的互操作性,允许 `import` 默认导出和 `require` 的混用
    "sourceMap": true,//生成 `.map` 源映射文件,便于调试
    "baseUrl": "./",//解析的基础路径
    "strict": true,//启用所有严格类型检查选项(包括 `noImplicitAny`、`strictNullChecks` 等)
    "resolveJsonModule": true,//允许导入 JSON 文件
    "allowSyntheticDefaultImports": true,//允许从没有默认导出的模块中默认导入(通常用于兼容 CommonJS 模块)
    "paths": {//配置模块路径别名
      "@/*": [
        "src/*"//将 `@/` 映射到 `src/` 目录
      ],
    }
  },
  "include": [//TypeScript 需要处理的文件
    "../../**/*.d.ts",//所有的类型声明文件
    "../../**/*.ts",//所有的 TypeScript 文件
    "../../**/*.tsx"//所有的 TypeScript + JSX 文件
  ],
  "exclude": ["node_modules"]
}

14. TypeScript 中的声明文件(.d.ts)有什么作用?

声明文件(通常以 .d.ts 扩展名结尾)用于描述已有 JavaScript 代码库的类型信息。它们提供了类型定义和元数据,以便在 TypeScript 项目中使用这些库时获得智能感知和类型安全。

  • 提供 JavaScript 库的类型信息
  • 不包含实现,只有类型声明
  • 使 TypeScript 能够理解现有的 JavaScript 代码

15. 解释 TypeScript 中的 keyof 和 typeof 操作符

  • keyof: 获取类型的所有键的联合类型

  • typeof: 在类型上下文中获取变量的类型

    typescript 复制代码
    interface Person {
      name: string;
      age: number;
    }
    type PersonKeys = keyof Person; // "name" | "age"
    
    const me = { name: "John", age: 30 };
    type MeType = typeof me; // { name: string; age: number }

16. TypeScript中的可选参数和默认参数是什么

  • 可选参数允许函数中的某些参数不传值,在参数后面加上问号?表示可选。
  • 默认参数允许在声明函数时为参数指定默认值,这样如果调用时未提供参数值,则会使用默认值。

可选参数示例:

ts 复制代码
function greet(name: string, greeting?: string) {
  if (greeting) {
    return `${greeting}, ${name}!`;
  } else {
    return `Hello, ${name}!`;
  }
}

默认参数示例:

ts 复制代码
function greet(name: string, greeting: string = "Hello") {
  return `${greeting}, ${name}!`;
}

17. 介绍TypeScript中的可选属性、只读属性和类型断言

  • 可选属性 使用 ? 来标记一个属性可以存在,也可以不存在。
  • 只读属性 使用 readonly 关键字来标记一个属性是只读的。
  • 类型断言 允许将一个实体强制指定为特定的类型,使用 <Type>value as Type

代码示例:

ts 复制代码
// 可选属性
interface Person {
  name: string;
  age?: number; // 可选属性
}

// 只读属性
interface Point {
  readonly x: number;
  readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // Error: 只读属性无法重新赋值

// 类型断言
let someValue: any = "hello";
let strLength: number = (someValue as string).length;

18. 如何处理可空类型(nullable types)和undefined类型,如何正确处理这些类型以避免潜在错误

在TypeScript中,可空类型是指一个变量可以存储特定类型的值,也可以存储nullundefined。(通过使用可空类型,开发者可以明确表达一个变量可能包含特定类型的值,也可能不包含值(即为nullundefined)。这有助于提高代码的可读性,并使得变量的可能取值范围更加清晰明了)。

为了声明一个可空类型,可以使用联合类型(Union Types),例如 number | nullstring | undefined。 例如:

ts 复制代码
let numberOrNull: number | null = 10; 
numberOrNull = null; // 可以赋值为null 
    
let stringOrUndefined: string | undefined = "Hello"; 
stringOrUndefined = undefined; // 可以赋值为undefined

19. 什么是命名空间(Namespace)和模块(Module)

模块

  • 在一个大型项目中,可以将相关的代码组织到单独的文件,并使用模块来导入和导出这些文件中的功能。
  • 在一个 Node.js 项目中,可以使用 import 和 export 关键字来创建模块,从而更好地组织代码并管理依赖关系。

命名空间

  • 在面向对象的编程中,命名空间可以用于将具有相似功能或属性的类、接口等进行分组,以避免全局命名冲突。
  • 这在大型的 JavaScript 或 TypeScript 应用程序中特别有用,可以确保代码结构清晰,并且不会意外地重复定义相同的名称。

模块提供了一种组织代码的方式,使得我们可以轻松地在多个文件中共享代码,

命名空间则提供了一种在全局范围内组织代码的方式,防止命名冲突。

模块示例:

ts 复制代码
// greeter.ts
export function sayHello(name: string) {
  return `Hello, ${name}!`;
}
// app.ts
import { sayHello } from './greeter';
console.log(sayHello('John'));

命名空间示例:

ts 复制代码
// greeter.ts
namespace Greetings {
  export function sayHello(name: string) {
    return `Hello, ${name}!`;
  }
}
// app.ts
<reference path="greeter.ts" />
console.log(Greetings.sayHello('John'));

在上面的示例中:

  • 使用模块时,我们可以使用 exportimport 关键字来定义和引入模块中的函数或变量。
  • 而在命名空间中,我们使用 namespace 来创建命名空间,并且需要在使用之前使用 <reference path="file.ts" />来引入命名空间。

20. 索引类型(Index Types)是什么,好处有什么

索引类型允许我们在 TypeScript 中创建具有动态属性名称的对象,并且能够根据已知的键来获取相应的属性类型。 好处:

1.动态属性访问

在处理动态属性名的对象时,可以使用索引类型来实现类型安全的属性访问。例如,当从服务器返回的 JSON 数据中提取属性时,可以利用索引类型来确保属性名存在并获取其对应的类型。

2.代码重用

当需要创建通用函数来操作对象属性时,索引类型可以帮助我们实现更加通用和灵活的代码。例如,一个通用的函数可能需要根据传入的属性名称获取属性值,并进行特定的处理。

ts 复制代码
interface ServerData {
  id: number;
  name: string;
  age: number;
  // 可能还有其他动态属性
}
function getPropertyValue(obj: ServerData, key: keyof ServerData): void {
  console.log(obj[key]); // 确保 obj[key] 的类型是正确的 // 这里可以直接使用索引类型来获取属性值
}

3.动态扩展对象

当需要处理来自外部来源(比如 API 响应或数据库查询)的动态数据时,索引类型可以让我们轻松地处理这种情况,而不必为每个可能的属性手动定义类型。

ts 复制代码
interface DynamicObject {
  [key: string]: number | string; // 允许任意属性名,但属性值必须为 number 或 string 类型

}
function processDynamicData(data: DynamicObject): void {
  for (let key in data) {
    console.log(key + ": " + data[key]); // 对任意属性进行处理
  }
}

4.类型安全性

索引类型可以增强代码的类型安全性,因为它们可以捕获可能的属性名拼写错误或键不存在的情况。

5.映射类型

TypeScript 还提供了映射类型(Mapped Types)的概念,它们利用索引类型可以根据现有类型自动生成新类型。这在创建新类型时非常有用,特别是当需要在现有类型的基础上添加或修改属性时。

21. const和readonly的区别

当在TypeScript中使用constreadonly时,它们的行为有一些显著的区别:

  • const:

    • const用于声明常量值。一旦被赋值后,其值将不能被重新赋值或修改。
    • 常量必须在声明时就被赋值,并且该值不可改变。
    • 常量通常用于存储不会发生变化的值,例如数学常数或固定的配置值。
ts 复制代码
const PI = 3.14;
PI = 3.14159; // Error: 无法重新分配常量
  • readonly:

    • readonly关键字用于标记类的属性,表明该属性只能在类的构造函数或声明时被赋值,并且不能再次被修改。
    • readonly属性可以在声明时或构造函数中被赋值,但之后不能再被修改。
    • readonly属性通常用于表示对象的某些属性是只读的,防止外部代码修改这些属性的值。
ts 复制代码
class Person {
    readonly name: string;

    constructor(name: string) {
        this.name = name; // 可以在构造函数中赋值
    }
}

let person = new Person("Alice");
person.name = "Bob"; // Error: 无法分配到"name",因为它是只读属性

总结来说,const主要用于声明常量值,而readonly则用于标记类的属性使其只读。

22. TypeScript中的this有什么需要注意的

在TypeScript中,与JavaScript相比,this的行为基本上是一致的。然而,TypeScript提供了类型注解和类型检查,可以帮助开发者更容易地理解和处理this关键字的使用。

在noImplicitThis为true 的情况下,必须声明 this 的类型,才能在函数或者对象中使用this。

Typescript中箭头函数的 this 和 ES6 中箭头函数中的 this 是一致的。

在TypeScript中,当将noImplicitThis设置为true时,意味着在函数或对象中使用this时,必须显式声明this的类型。这一设置可以帮助开发者更明确地指定this的类型,以避免因为隐式的this引用而导致的潜在问题。

具体来说,如果将noImplicitThis设置为true,则在下列情况下必须显式声明this的类型:

  • 在函数内部使用this时,需要使用箭头函数或显示绑定this。
  • 在某些类方法或对象方法中,需要明确定义this的类型。

示例代码如下所示:

ts 复制代码
class MyClass {
  private value: number = 42;

  public myMethod(this: MyClass) {
    console.log(this.value);
  }

  public myMethod2 = () => {
    console.log(this.value);
  }
}

let obj = new MyClass();
obj.myMethod(); // 此处必须传入合适的 this 类型

通过将noImplicitThis设置为true,TypeScript要求我们在使用this时明确指定其类型,从而在编译阶段进行更严格的类型检查,帮助避免一些可能出现的错误和不确定性。

注:noImplicitThis是TypeScript编译器的一个配置选项,用于控制在函数或对象方法中使用this时的严格性。当将noImplicitThis设置为true时,意味着必须显式声明this的类型,否则会触发编译错误。

23. TypeScript中的协变、逆变、双变和抗变是什么

在TypeScript中,协变(Covariance)逆变(Contravariance)双变(Bivariance)抗变(Invariance 是与类型相关的概念,涉及到参数类型的子类型关系。下面对这些概念进行解释,并提供示例代码。

协变(Covariance)

  • 区别:协变意味着子类型可以赋值给父类型。
  • 应用场景:数组类型是协变的,因此可以将子类型的数组赋值给父类型的数组。

协变表示类型T的子类型可以赋值给类型U,当且仅当T是U的子类型。在TypeScript中,数组是协变的,这意味着可以将子类型的数组赋值给父类型的数组。

ts 复制代码
let subtypes: string[] = ["hello", "world"];
let supertype: Object[] = subtypes; // 数组是协变的,这是合法的

逆变(Contravariance)

  • 区别:逆变意味着超类型可以赋值给子类型。
  • 应用场景:函数参数类型是逆变的,因此可以将超类型的函数赋值给子类型的函数。

逆变表示类型T的超类型可以赋值给类型U,当且仅当T是U的子类型。在TypeScript中,函数参数是逆变的,这意味着可以将超类型的函数赋值给子类型的函数。

ts 复制代码
type Logger<T> = (arg: T) => void;
let logNumber: Logger<number> = (x: number) => console.log(x);
let logAny: Logger<any> = logNumber; // 函数参数是逆变的,这是合法的

双变(Bivariance)

  • 区别:双变允许参数类型既是协变又是逆变的。
  • 应用场景:对象类型是双变的,这意味着可以将子类型的对象赋值给父类型的对象,同时也可以将超类型的对象赋值给子类型的对象。

双变允许参数类型既是协变又是逆变的。在TypeScript中,普通对象类型是双变的,这意味着可以将子类型的对象赋值给父类型的对象,并且可以将超类型的对象赋值给子类型的对象。

ts 复制代码
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

let animal: Animal = { name: "Animal" };
let dog: Dog = { name: "Dog", breed: "Labrador" };

animal = dog; // 对象类型是双变的,这是合法的
dog = animal; // 对象类型是双变的,这也是合法的

抗变(Invariance)

  • 区别:抗变表示不允许类型之间的任何赋值关系。
  • 应用场景:通常情况下,基本类型和类类型是抗变的。

抗变表示不允许类型T和U之间的任何赋值关系,即T既不是U的子类型,也不是U的超类型。在TypeScript中,一般情况下,基本类型类类型是抗变的。

ts 复制代码
let x: string = "hello";
let y: string = x; // 这是合法的

let a: Animal = { name: "Animal" };
let b: Animal = a; // 这也是合法的

24. TypeScript中的静态类型和动态类型有什么区别

  • 静态类型是在 编译期间 进行类型检查,可以在编辑器或 IDE 中发现大部分类型错误。
  • 动态类型是在 运行时 才确定变量的类型,通常与动态语言相关联。

静态类型(Static Typing)

  • 定义:静态类型是指在编译阶段进行类型检查的类型系统,通过类型注解或推断来确定变量、参数和返回值的类型。
  • 特点:静态类型能够在编码阶段就发现大部分类型错误,提供了更好的代码健壮性和可维护性。
  • 优势:可以在编辑器或 IDE 中实现代码提示、自动补全和类型检查,帮助开发者减少错误并提高代码质量。

动态类型(Dynamic Typing)

  • 定义:动态类型是指在运行时才确定变量的类型,通常与动态语言相关联,允许同一个变量在不同时间引用不同类型的值。
  • 特点:动态类型使得变量的类型灵活多变,在运行时可以根据上下文或条件动态地改变变量的类型。
  • 优势:动态类型可以带来更大的灵活性,适用于一些需要频繁变化类型的场景。

区别总结

  • 时机差异:静态类型在编译期间进行类型检查,而动态类型是在运行时才确定变量的类型。
  • 代码稳定性:静态类型有助于在编码阶段发现大部分类型错误,提高代码稳定性;动态类型对类型的要求较为灵活,但可能增加了代码的不确定性。
  • 使用场景:静态类型适合于大型项目和团队,能够提供更强的类型安全性;动态类型适用于快速原型开发和灵活多变的场景,能够更快地迭代和测试代码。

25. 如何约束泛型参数的类型范围

可以使用泛型约束(extends关键字)来限制泛型参数的类型范围,确保泛型参数符合某种特定的条件。

代码示例:

ts 复制代码
interface Lengthwise {
  length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}
loggingIdentity({length: 10, value: 3});  // 参数满足 Lengthwise 接口要求,可以正常调用

26. 什么是泛型约束中的 keyof 关键字,举例说明其用法。

  • keyof 是 TypeScript 中用来获取对象类型所有键(属性名)的操作符。
  • 可以使用 keyof 来定义泛型约束,限制泛型参数为某个对象的键。

代码示例:

ts 复制代码
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
let x = { a: 1, b: 2, c: 3 };
getProperty(x, "a"); // 正确
getProperty(x, "d"); // 错误:Argument of type '"d"' is not assignable to parameter of type '"a" | "b" | "c"'

27. 什么是条件类型(conditional types),能够举例说明其使用场景吗

  • 条件类型是 TypeScript 中的高级类型操作符,可以根据一个类型关系判断结果类型。
  • 例如,可以使用条件类型实现一个类型过滤器,根据输入类型输出不同的结果类型。

代码示例:

ts 复制代码
type NonNullable<T> = T extends null | undefined ? never : T;
type T0 = NonNullable<string | null>;  // string
type T1 = NonNullable<string | null | undefined>;  // string
type T2 = NonNullable<string | number | null>;  // string | number

28. 什么是装饰器,有什么作用,如何在TypeScript中使用类装饰器

  • 装饰器是一种特殊类型的声明,可以附加到类、方法、访问符、属性或参数上,以修改其行为。
  • 在 TypeScript 中,装饰器提供了一种在声明时定义如何处理类的方法、属性或参数的机制。

如何在 TypeScript 中使用类装饰器:

ts 复制代码
function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    newProperty = "new property";
    hello = "override";
  };
}

@classDecorator
class Greeter {
  property = "property";
  hello: string;
  constructor(m: string) {
    this.hello = m;
  }
}
console.log(new Greeter("world")); // 输出 { property: 'property', hello: 'override', newProperty: 'new property' }

29. 类装饰器和方法装饰器的执行顺序是怎样的

  • 当有多个装饰器应用于同一个声明时(比如一个类中的方法),它们将按照自下而上的顺序应用。
  • 对于方法装饰器,从顶层方法开始依次向下 递归调用方法装饰器函数。

30. 装饰器工厂是什么,请给出一个装饰器工厂的使用示例

  • 装饰器工厂是一个返回装饰器的函数。它可以接受参数,并根据参数动态生成装饰器。

以下是一个简单的装饰器工厂示例:

ts 复制代码
function color(value: string) {
  return function (target: any, propertyKey: string) {
    // ... 在此处使用 value 和其他参数来操作装饰目标
  };
}

class Car {
  @color('red')
  brand: string;
}

31. TypeScript 中的 never 类型是什么?

  • 表示永远不会发生的值的类型

  • 常用于抛出异常或无限循环的函数返回类型

    typescript 复制代码
    function error(message: string): never {
      throw new Error(message);
    }
相关推荐
阳光是sunny9 小时前
手把手写JavaScript压缩的核心原理
前端·javascript·面试
烛阴10 小时前
深入浅出,教你用JS/TS轻松搞定Excel与XML互转
前端·javascript·typescript
追逐时光者10 小时前
面试时该如何做好自我介绍呢?附带介绍样板示例!!!
面试
JavaGuide12 小时前
诶,越来越多的程序员面试也开始卡学历了!
前端·后端·面试
Warren9812 小时前
Redis 八股面试题
java·数据库·redis·spring·缓存·面试·eclipse
倔强青铜三12 小时前
苦练Python第28天:列表推导式
人工智能·python·面试
倔强青铜三12 小时前
苦练Python第30天:浅拷贝 VS 深拷贝
人工智能·python·面试
倔强青铜三12 小时前
苦练Python第29天:一行代码生成字典与集合!
人工智能·python·面试
讨厌吃蛋黄酥13 小时前
🔥 前端面试必杀技:单例模式实现 Storage 的两种终极方案
前端·javascript·面试
cxr82814 小时前
Vercel AI SDK 3.0 学习入门指南
前端·人工智能·学习·react.js·typescript·npm·reactjs