2025前端面试 —— TS 篇

200 阅读6分钟

一、type 和 interface 的区别

在 TypeScript 里,type(类型别名)和 interface(接口)都可用于定义对象类型。

1. 定义语法

  • type:使用 type 关键字来创建类型别名,它能定义各种类型,像基本类型、联合类型、交叉类型、元组类型等。
// 基本类型别名
type MyString = string;

// 联合类型
type StringOrNumber = string | number;

// 对象类型
type Person = {
    name: string;
    age: number;
};
  • interface:运用 interface 关键字定义接口,主要用于定义对象的结构。
interface Person {
    name: string;
    age: number;
}

2. 扩展方式

  • type:借助交叉类型(&)来实现扩展。
type Animal = {
    name: string;
};

type Dog = Animal & {
    breed: string;
};

const myDog: Dog = {
    name: 'Buddy',
    breed: 'Golden Retriever'
};
  • interface:使用 extends 关键字进行扩展。
interface Animal {
    name: string;
}

interface Dog extends Animal {
    breed: string;
}

const myDog: Dog = {
    name: 'Buddy',
    breed: 'Golden Retriever'
};

3. 重复定义

  • type:不能重复定义相同名称的类型别名,重复定义会引发编译错误。
type Point = {
    x: number;
    y: number;
};

// 下面这行代码会报错
// type Point = {
//     z: number;
// };
  • interface:可以重复定义相同名称的接口,这些定义会自动合并。
interface User {
    name: string;
}

interface User {
    age: number;
}

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

4. 对基本类型的支持

  • type:能够定义基本类型的别名,还可以定义联合类型、交叉类型、元组类型等复杂类型。
// 基本类型别名
type MyNumber = number;

// 联合类型
type StringOrBoolean = string | boolean;

// 元组类型
type Tuple = [string, number];
  • interface:主要用于定义对象类型,无法直接定义基本类型、联合类型或元组类型。不过可以通过定义对象属性的方式间接支持部分复杂类型。
// 无法直接定义联合类型,但可以这样使用
interface UnionWrapper {
    value: string | number;
}

5. 实现方式

  • type:类不能直接实现类型别名,不过可以通过交叉类型或联合类型组合多个类型别名来实现。
type Printable = {
    print: () => void;
};

class MyClass implements Printable {
    print() {
        console.log('Printing...');
    }
}
  • interface:类可以直接实现接口,实现接口时,类必须包含接口中定义的所有属性和方法。
interface Printable {
    print: () => void;
}

class MyClass implements Printable {
    print() {
        console.log('Printing...');
    }
}

6. 映射类型支持

  • type:在定义映射类型时更常用,因为它可以直接使用类型操作符来创建新类型。
type ReadonlyPerson = {
    readonly [P in keyof Person]: Person[P];
};
  • interface:在映射类型方面的支持不如 type 灵活,通常使用 type 来实现映射类型。

二、declare 关键字的作用

declare 关键字主要用于告诉编译器某个变量、函数、类、接口或者模块等的类型信息,而无需提供具体的实现。它在处理外部库、全局变量和旧代码时非常有用,能让 TypeScript 编译器理解这些外部资源的类型,从而进行类型检查。

1. 声明全局变量

当你使用一些全局变量(例如在 HTML 文件里通过 <script> 标签引入的库所创建的全局变量)时,TypeScript 编译器或许不了解这些变量。此时可以用 declare 来声明这些变量的类型。

// 声明一个全局变量 jQuery
declare const jQuery: (selector: string) => any;

// 使用全局变量
const $ = jQuery('body');

2. 声明函数

若要使用外部定义的函数,但是不想在 TypeScript 里实现它,就可以使用 declare 声明函数的签名。

// 声明一个全局函数
declare function greet(name: string): void;

// 使用声明的函数
greet('John');

3. 声明类

在引用外部类时,若不想实现这个类,可使用 declare 声明类的结构。

// 声明一个类
declare class Person {
    constructor(name: string, age: number);
    getName(): string;
    getAge(): number;
}

// 使用声明的类
const person = new Person('Alice', 25);
console.log(person.getName());

4. 声明接口

在 TypeScript 中,接口常用来定义对象的形状。使用 declare 可以声明全局接口。

// 声明一个接口
declare interface User {
    name: string;
    age: number;
}

// 使用声明的接口
const user: User = { name: 'Bob', age: 30 };

5. 声明模块

要是使用外部模块,却不想引入其具体实现,就可以使用 declare 声明模块。

// 声明一个模块
declare module 'lodash' {
    export function cloneDeep<T>(value: T): T;
}

// 使用声明的模块
import { cloneDeep } from 'lodash';
const original = { a: 1, b: { c: 2 } };
const cloned = cloneDeep(original);

6. 声明命名空间

在旧版本的 TypeScript 里,命名空间用于组织代码。使用 declare 可以声明全局命名空间。

// 声明一个命名空间
declare namespace MyNamespace {
    export function doSomething(): void;
}

// 使用声明的命名空间
MyNamespace.doSomething();

三、泛型及其作用

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而是在使用的时候再指定类型的一种特性。通过泛型,你可以编写通用的代码,而不是为每一种具体的类型都编写一套代码。

使用场景

  • 函数:创建可处理多种数据类型的函数。
  • :构建可操作不同数据类型的类。
  • 接口:定义能适配多种类型的接口。

语法和示例

泛型函数

下面是一个简单的泛型函数示例,该函数用于返回传入的参数:

function identity<T>(arg: T): T {
    return arg;
}

// 使用泛型函数
let output1 = identity<string>("myString");
let output2 = identity<number>(100);

console.log(output1); 
console.log(output2); 

在上述代码中,<T> 是泛型类型变量,它代表一种类型。在调用 identity 函数时,我们可以通过 <string> 或 <number> 来指定 T 的具体类型。

泛型类

以下是一个泛型类的示例,该类表示一个简单的栈结构:

class Stack<T> {
    private items: T[] = [];

    push(item: T) {
        this.items.push(item);
    }

    pop(): T | undefined {
        return this.items.pop();
    }
}

// 使用泛型类
let numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); 

let stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");
console.log(stringStack.pop()); 

在这个例子中,Stack 类使用了泛型 T,可以存储任意类型的数据。在创建 Stack 实例时,通过 <number> 或 <string> 来指定存储的数据类型。

泛型接口

下面是一个泛型接口的示例,该接口定义了一个函数类型:

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
console.log(myIdentity(10)); 

在这个示例中,GenericIdentityFn 是一个泛型接口,它定义了一个函数类型,该函数接收一个类型为 T 的参数并返回一个类型为 T 的值。

泛型约束

有时候,你可能希望对泛型类型进行一些限制,这时可以使用泛型约束。例如,你希望泛型类型必须具有某个属性:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); 
    return arg;
}

loggingIdentity("hello"); 
// loggingIdentity(10); // 报错,因为 number 类型没有 length 属性

在上述代码中,T extends Lengthwise 表示 T 必须是 Lengthwise 接口的子类型,即 T 类型必须具有 length 属性。