2025前端面试题-TS理论篇

917 阅读10分钟

以下为TS理论篇面试考察点总结,具体知识点不会太详细,主要梳理面试核心考察点,为面试做准备。

一、核心类型系统

1. 基础类型

1.1 原始类型

let name: string = 'Tom';
let age: number = 30;
let isActive: boolean = true;
let nithing: null = null;
let notDefined: undefined = undefined;
let uniqueKey: symbol = Symbol('id');

1.2 数组与元组

// 数组
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ['a', 'b', 'c'];
// 元组(固定长度和类型)
let person: [string, number] = ['Tom', 10];
person[0] = 'Jerry';     // ✅
person[1] = '20';       //  ❌ 类型错误

1.3 特殊类型

类型特点使用场景
any禁用类型检查,与原生 JS 行为一致迁移 JS 项目、快速原型设计
unknown类型安全的 any,使用前必须断言或类型检查接收第三方库数据、API 响应处理
never表示永远不会发生的值函数抛出异常、穷尽检查
void函数无返回值(与 undefined兼容但不等价)函数副作用操作
// any vs unknown
let anyValue: any = 'hello';
anyValue.toFixed(2); // ❌ 运行时错误

let unknownValue: unknown = 'hello';
if (typeof unknownValue === 'string') {
    unknownValue.toUpperCase();  // ✅ 优先类型检查
}

// never 使用示例
function throwError(message: string) {
    throw new Error(message);
}

// void 函数
function logMessage(msg: string) {
    console.log(msg);
    // 等价于 undefined,但 null 会报错
}

1.4 void 与 undefined

// void 表示函数没有返回值
type VoidFunc = () => void;
const fn: VoidFunc = () => true; // ✅ 兼容但返回类型仍为 void

// undefined 必须显式返回
const requireUndefined = (): undefined => {
    return undefined; // ✅
    // return;       // ❌ 缺少返回值
}

2. 类型推断与断言

2.1 类型推断

// 自动推断变量类型
let x = 10;        // 推断为 number
// 函数返回类型推断
function add(a: number, b: number) {
    return a + b; // 推断返回 number
}

2.2 类型断言

// as 断言(推荐)
const element = document.getElementById('root') as HTMLDivElement;

// 非空断言(!)
const user!: { name: string }; // 强制认为非空(危险!)
const el = document.querySelector('.btn')!;

// const 断言(值不可变)
let arr = [1, 2] as const; // 类型变为 readonly [1, 2]
arr.push(3); // ❌ 编译错误

2.3 双重断言的风险

const input = document.querySelector('input[type="text"]');

// 双重断言绕过类型检查(慎用!)
const value = (input as any) as string;

// 安全替代方案:类型守卫
if (input instanceof HTMLInputElement) {
    const safeValue = input.value;  // ✅ 类型安全
}

3. 类型兼容性

3.1 结构化类型(鸭子类型)

interface Person {
    name: string;
    age: number;
}

let tom = { name: 'Tom', age: 20, gender: 'M' };
const bob: Person = tom; // ✅ 额外属性不破坏兼容性

// 空对象兼容(特殊情况)
const empty = {};
const p: Person = empty; // ❌ 缺少必要属性

3.2 函数类型兼容

type Handler = (data: string) => void;

// ✅ 参数兼容:目标类型参数 < 源类型参数
const handleEvent: Handler = (data: string | number) => console.log(data);

// ❌ 错误示例:源类型参数 < 目标类型参数
const handler: (data: string | number) => void = (data: string) => console.log(data);

3.3 逆变与协变

开启strictFunctionTypes后:

  • 参数类型:逆变(接收更具体的类型)
  • 返回类型:协变(返回更宽泛的类型)
// 逆变演示(参数兼容)
type AnimalHandler = (animal: Animal) => void;
type DogHandler = (dog: Dog) => void;

let animalFunc: AnimalHandler = (a: Animal) => {};
let dogFunc: DogHandler = (d: Dog) => {};

dogFunc = animalFunc; // ✅ 安全(目标函数能处理更多类型)
animalFunc = dogFunc; // ❌ 危险(关闭strictFunctionTypes时允许)

// 协变示例(返回值兼容)
type AnimalCreator = () => Animal;
type DogCreator = () => Dog;

let createAnimal: AnimalCreator = () => new Animal();
let createDog: DogCreator = () => new Dog();

createAnimal = createDog; // ✅ 安全(返回值更具体)
createDog = createAnimal; // ❌ 错误(可能返回非Dog对象)

常见面试题

Q1:any和unknown的主要区别是什么?

A:any禁用所有类型检查,相当于退化到JS;unknown要求在使用前显式断言或类型检查,确保操作的安全性。

Q2:为什么使用元组而不是普通数组?

A:元组精确维护元素的位置和类型,例如[string, number]确保第一个元素是字符串,第二个是数字,防止越界访问和类型错位。

Q3:void和undefined在函数返回值中的区别?

A:void表示函数没有有效返回值(可返回undefinednull),而undefined要求必须显式返回undefined

Q4:双重断言为什么危险?

A:as any as T 绕过类型系统检查,可能导致运行时错误。应优先使用类型守卫或正确的类型声明

Q5:协变和逆变的实践意义?

A:协变确保子类型可替换父类型(安全),逆变保证函数参数接受更具体类型(避免类型错误)。strictFunctionTypes强调参数逆变,提升类型安全

二、高级类型编程

1. 泛型

1.1 泛型函数/类/接口

// 泛型函数
function identity<T>(arg: T): T { return args; }
const num = identity<number>(42);  // 显式指定
const str = identity("hello");     // 自动推断为 string

// 泛型接口
interface KeyValuePair<K, V> {
    key: K;
    value: V;
}
const pair: KeyValuePair<number, string> = { key: 1, value: 'A' };

// 泛型类
class DataWrapper<T> {
    constructor(public value: T) {}
    getValue(): T { return this.value; }
}
const wrapper = new DataWrapper<boolean>(true);

1.2 泛型约束与默认类型

// 约束类型必须具有length属性
interface LengthWise { length: number; }
function logLength<T extends Lengthwise>(obj: T): void {
    console.log(obj.length);
}
logLength([1, 2]);  // ✅
logLength(3);       // ❌ 类型‘number’不满足约束

// 默认类型参数
interface Pagination<T = string> {
    data: T[];
    page: number;
}
const pagination: Pagination = { data: ['a', 'b'], page: 1; } // T默认string

1.3 类型参数推导(infer)

在条件类型中提取嵌套类型

// 获取函数返回类型
type ReturnType<T> = T extends (...args: any) => infer R ? R : never;
// 获取数组元素类型
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type StrElement = ArrayElement<string[]>; // string

2. 工具类型与类型编程

内置工具类型实现常见类型转换

2.1 内置工具类型

工具类型作用等价写法
Partial<T>所有属性变为可选{ [P in keyof T]?: T[P] }
Required<T>所有属性变为必填{ [P in keyof T]-?: T[P] }
Readonly<T>所有属性变为只读{ readonly [P in keyof T]: T[P] }
Pick<T, K>选取T中指定属性K{ [P in K]: T[P] }
Omit<T, K>排除T中指定属性KPick<T, Exclude<keyof T, K>>
Record<K, T>创建键为K类型,值为T类型的对象{ [P in K]: T }
Exclude<T, U>从联合类型T中排除U类型T extends U ? never : T
Extract<T, U>从联合类型T中提取U类型T extends U ? T : never
实战示例
type User = {
    id: number;
    name: string;
    email?: string;
};
// 创建用户更新类型(所有属性可选)
type UserUpdate = Partial<User>;
// 只选取ID和名称
type UserPreview = Pick<User, 'id' | 'name'>;
// 排除email创建核心类型
type CoreUser = Omit<User, 'email'>;
// 创建权限映射
type PermissionLevel = 'read' | 'write' | 'admin';
const permissions: Record<PermissionLevel, boolean> = {
    read: true;
    write: false;
    admin: true;
};

2.2 映射类型

通过遍历修改已有类型的属性

// 将所有属性转为getter函数
type Getters<T> = {
    [K in keyof T as `get&{Capitalize<string & K>}`]: () => T[K]
};

interface Person {
    name: string;
    age: number;
}

type PersonGetters = Getters<Person>;
// 等价于
{
    getName: () => string;
    getAge: () => number;
}

2.3 条件类型

实现类型逻辑分支

// 处理Promise返回值
type AsyncResult<T> = T extends Promise<infer U> ? U : T;
// 深度可选
type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T

2.4 模型字面量类型

创建组合字符串类型

type HttpMethod = 'GET' | 'POST' | 'PUT';
type ApiRoute = `/api/${string}`;

type ApiEndpoint = `${HttpMethod} ${ApiRoute}`;
const endpoint: ApiEndpoint = 'GET /api/users';

// 结合映射类型创建事件系统
type Event = 'click' | 'scroll';
type HandlerType = `${Event}Handler`;
// 'clickHandler' | 'scrollHandler'

3. 类型守卫与类型收窄

缩小变量在特定作用域的类型范围

3.1 内置守卫

// typeof
function padLeft(value: string, padding: string | number) {
    if (typeof padding === 'number') {
        return ' '.repeat(padding) + value; // padding: number
    }
    return padding + value; // padding: string
}

// instanceof
class Animal {}
class Bird extends Animal { fly() {} }
function move(animal: Animal) {
    if(animal instanceof Bird){
        animal.fly(); // animal: Bird
    }
}

// in 操作符
interface Fish { swim(): void }
interface Dog { run(): void }
function action(pet: Fish | Dog) {
    if ('swim' in pet) {
        pet.swim(); // pet: Fish
    }
}

3.2 自定义类型守卫

// 类型谓词语法: args is Type
function isString(value: unknown): value is string {
    return typeof value === 'string';
}

function process(input: string | number) {
    if(isString(input)) {
        input.toUppercase(); // input: string
    }
}

3.3 可辨识联合

通过公共字面量字段区分类型

type Shape = 
    | { kind: 'circle'; radius: number }
    | { kind: 'square'; side: number };
function getArea(shape: Shape) {
    switch (shape.kind) {
        case 'circle':
            return Math.PI * shape.radius ** 2; // shape: Circle
        case 'square':
            return shape.side ** 2; // shape: Square
    }
}

常见面试题

Q1:Partial<T>Omit<T, K>实现原理有什么不同?

A:Partial<T>使用in keyof映射所有属性为可选,而Omit<T,K>通过Exclude<keyof T, K>选出保留的键名,再用Pick构建新类型。

Q2:什么时候用infer

A:当需要在条件类型中捕获嵌套类型时使用,如提取函数返回类型、数组元素类型、Promise结果等无法直接访问的内部类型。

Q3:如何理解T extends U ? X : Y中的extends?

A:此处的extends是类型的约束检查,不是类继承。表示类型T是否能赋值给类型U,满足则返回X类型,否则Y类型。

Q4:可辨识联合相比普通联合类型有什么优势?

A:通过共享字面量字段(如kind),TS能自动进行穷尽性检查,避免遗漏分支,同时提供精确的类型收窄能力。

Q5:自定义类型守卫函数为什么要用is关键字?

A:value is Type语法向编译器明确该函数的作用是进行类型收窄,使其在条件分支中触发类型推断,否则会被视为普通布尔值返回。

三、面向对象与设计

1. 类与继承

TypeScript增强了ES6类的类型系统,提供完整的OOP支持

1.1 访问修饰符

控制类成员的可见性和可变性

修饰符类内部子类实例不可重新赋值
public
protected
private
readonly
class Animal {
    public name: string;      // 公开访问
    protected age: number;    // 仅类及子类可访问
    private id: string;       // 仅类内部访问
    readonly species: string; // 初始化后不可修改
    
    constructor(name: string) {
        this.name = name;
        this.id = genderateUUID();
        this.species = 'Animal';
    }
}

class Dog extends Animal {
    constructor(name: string) {
        super(name);
        console.log(this.age);  // ✅ 子类可访问protected
        console.log(this.id);   // ❌ private成员不可访问
    }
}

const dog = new Dog('Nice');
dog.name = 'Bad'; // ✅
dog.species = 'Taidy'; // ❌ readonly属性

1.2 抽象类与抽象方法

定义规范而不实现细节,强制子类实现约定

abstract class Shape {
    abstract getArea(): number;  // 抽象方法无实现
    
    // 抽象类可包含具体方法
    printArea() {
        console.log(`Area: ${this.getArea()}`);
    }
}

class Circle extends Shape {
    constructor(private radius: number) {
        super();
    }
    
    // 必须实现抽象方法
    getArea() {
        return Math.PI * this.radius ** 2;
    }
}

const shape = new Shape(); // ❌ 抽象类不能实例化
const circle = new Circle(5);
circle.printArea(); // 输出:Area: 78.54

1.3 静态成员与静态块

类级别的属性和方法,通过类名直接访问

class Config {
    static apiUrl: string;
    static env: 'dev' | 'prod';
    
    // 静态初始化块 (TypeScript 4.4+)
    static {
        this.env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';
        this.apiUrl = this.env === 'prod' ? 'https://api.example.com' : 'http://localhost:3000';
    }
    
    static logConfig() {
        console.log(`Environment: ${this.env}, API: ${this.apiUrl}`);
    }
}

// 无需实例化直接访问
Config.logConfig();

2. 接口 vs 类型别名

理解两种类型定义方式

2.1 声明合并

// 接口自动合并
interface User {
    name: string;
}

interface User {
    age: number;
}

const user: User = {
    name: 'Tom',
    age: 30,
};

// 类型别名不可重复
type Person = { name: string };
type Person = { age: number }; // ❌ 重复标识符

2.2 扩展方式

// 接口扩展
interface Animal {
    name: string;
}

interface Dog extends Animal {
    breed: string;
}

// 类型别名扩展
type Cat = Animal & {
    color: string
}

// 接口扩展类型别名
interface Brid extends Animal {
    wingspan: number;
}

2.3 适用场景对比

​特性​​接口​​类型别名​
声明合并
扩展方式extends&(交叉类型)
实现类✓ (implements)
联合/元组等复杂类型✓ (type T = A)
映射类型✓ (type Readonly<T> = { ... })
最佳实践:
  • 定义对象形状优先用接口
  • 复杂类型组合(联合、元组)用类型别名
  • 库类型定义优先用接口(支持使用者扩展)

3. 装饰器

元编程工具,增强类/方法/属性的行为(需启用experimentalDecorators)

3.1 装饰器类型

// 类装饰器
@sealed
class Person {}

function sealed(constructor: Function) {
    Object.seal(conctructor);
    Object.seal(constructor.prototype);
}

// 方法装饰器
class MathOps {
    @log
    add(a: number, b: number) {
        return a + b;
    }
}

function log(target: any, key: string, desc: PropertyDescriptor) {
    const original = desc.value;
    desc.value = function (..args: any[]) {
        console.log(`Calling ${key} with`, args);
        return original.apply(this, args);
    }
}

// 属性装饰器
class User {
    @format('Hello, %s')
    name: string;
}

function farmat(template: string) {
    return (target: any, key: string) => {
        let value = target[key];
        Object.defineProperty(target, key, {
            get: () => value;
            set: (v) => { value = template.replace('%s', v) },
        });
    };
}

// 参数装饰器
class Validator {
    validate(@required name: string) {}
}

function required(target: any, key: string, index: number) {
    // 存储元数据用于后续验证
}

3.2 装饰器工厂与执行顺序

// 装饰器工厂(返回装饰器函数)
function decoratorFactory(options: any){
    return function (target: any) {
        // 使用options配置
    }
}

@decoratorFactory({ level: 'high' })
class Example {}

// 执行顺序
// 1. 参数装饰器
// 2. 方法/属性装饰器
// 3. 类装饰器
// 多个同类型装饰器时,从上到下执行

常见面试题

Q1:什么时候该用抽象类而不是接口?

A:当需要共享具体实现时用抽象类(如公共方法),当仅需定义契约时用接口。抽象类适合有部分共同实现的类族,接口适合完全抽象的行为约定。

Q2:readonlyconst有什么区别?

A:const用于变量绑定不可重新赋值,readonly用于类属性初始化后不可修改。const是运行时约束,readonly是编译时约束。

Q3:装饰器在什么阶段执行?能修改类行为吗?

A:装饰器在定义时执行(非实例化时)。可以修改类行为,如添加/修改方法、属性,甚至返回新的构造函数替换原类。

Q4:接口声明合并有什么实际应用?

A:主要分为三个部分:

  1. 扩展第三方库类型定义;
  2. 拆分大型接口到多个文件;
  3. 为已有类型渐进添加功能。例如为winodw接口添加自定义属性:
declare global {
    interface Window {
        myLib: Record<string, any>;
    }
}

Q5:如何选择interfacetype

A:遵循两点准则:

  1. 需要声明合并或implements时用interface
  2. 需要联合类型时、元组或映射类型时用type 其他场景根据团队规范保持一致即可。

以上是TS理论篇面试考察点的内容,如有错误欢迎评论区指正,后续还有TS实战篇内容输出,欢迎关注👏。