TypeScript 已成为现代前端开发不可或缺的一部分,它通过提供静态类型检查,极大地提升了代码的可维护性、可读性和健壮性。在面试中,TypeScript 相关的问题常常用于考察开发者对类型系统、常见特性以及最佳实践的理解。
面试题涉及了 TypeScript 语言的各个方面,包括基本语法
、类型系统
、函数
、类
、模块化
、泛型
、装饰器
等。在面试中,常见的 TypeScript 面试题主要围绕以下几个方面展开:
类型系统:考察对 TypeScript 类型系统的理解,包括基本类型、联合类型、交叉类型、接口、类型别名、类型推断、类型守卫等。
函数和类:涉及函数参数类型、返回值类型、箭头函数、函数重载、类的定义、继承、访问修饰符等概念。
泛型:考察在函数、类和接口中如何使用泛型来增加代码的灵活性和复用性。
模块化:问题可能涉及 ES6 模块化的语法、导入导出方式以及模块解析等内容。
装饰器:了解对装饰器的使用,包括类装饰器、方法装饰器、属性装饰器以及参数装饰器的定义和应用。
编译配置:熟悉 tsconfig.json 中的配置选项,包括编译目标、模块系统、严格模式等。
工程化实践:了解 TypeScript 在项目中的实际应用,如与 JavaScript 的混用、第三方库的声明文件使用、类型声明等。
TypeScript数据类型
在TypeScript中,常见的数据类型包括以下几种:
-
基本类型:
number
: 表示数字,包括整数和浮点数。string
: 表示文本字符串。boolean
: 表示布尔值,即true
或false
。null
、undefined
: 分别表示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. interface
和 type
有什么区别?何时使用它们?
interface
和 type
是 TypeScript 中定义类型的两种主要方式,它们有很多相似之处,但也存在一些关键区别。
主要区别
特性 | interface | type |
---|---|---|
用途 | 定义对象类型或类实现接口 | 定义任意类型(包括对象、联合、基本类型等) |
声明合并 | 支持,同名的 interface 会自动合并 | 不支持,同名的 type 会报错 |
扩展方式 | 使用 extends 继承 |
使用 & 交叉类型 |
实现方式 | 可以被类实现 (implements ) |
不能被类直接实现 |
映射类型 | 不能直接使用映射类型 | 可以使用映射类型 |
可定义内容 | 属性、函数、索引签名、类的约束 | 所有类型,包括基本类型别名、对象类型、元组、联合类型、交叉类型、映射类型、条件类型、函数签名等 |
性能 | 在大型项目中性能更好 | 复杂类型可能影响性能 |
应用场景
interface
-
使用
interface
的场景:- 当你需要定义对象的形状时(尤其是用于公共 API 或库的类型定义)。
- 当你希望类型可以被多次声明并自动合并(例如,为第三方库添加模块增强时)。
- 当需要被类实现 (
implements
) 时。
-
定义对象形状(主要用途):
typescriptinterface Person { name: string; age: number; }
-
需要声明合并:
typescriptinterface User { name: string; } interface User { age: number; } // 最终 User 包含 name 和 age
-
需要被类实现 : "需要被类实现"是指接口可以作为类的设计规范,确保类具有特定的结构和行为,这是 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"); } }
-
与第三方库交互(扩展库的类型定义):
typescript// 扩展 window 对象 interface Window { myCustomProp: string; }
type
-
使用
type
的场景:- 当你需要定义基本类型别名 、联合类型 、交叉类型 、元组类型等复杂类型组合时。
- 当你需要定义函数类型时。
- 当你需要进行类型操作 (如通过
keyof
、typeof
等)。 - 高级类型变换,如
Pick<T, K>
、条件类型 - 定义复杂泛型逻辑
-
定义联合类型:
typescripttype Status = 'success' | 'error' | 'pending';
-
定义元组类型:
typescripttype Coordinates = [number, number];
-
使用映射类型:
typescripttype Readonly<T> = { readonly [P in keyof T]: T[P]; };
-
定义复杂类型组合:
typescripttype Tree<T> = { value: T; left?: Tree<T>; right?: Tree<T>; }
-
需要类型别名简化复杂类型:
typescripttype StringOrNumber = string | number;
最佳实践
对象类型优先用 interface
,更复杂的类型组合用 type
。
在大多数情况下,如果你只是定义对象的形状,使用 interface
更好,因为它在合并和声明方面更直观。对于其他非对象类型的定义,或者需要更灵活的类型组合时,使用 type
。
interface
是为"面向对象结构"设计的,type
是为"类型变换与表达"设计的。它们不是对立,而是互补。
3. any
和 unknown
有什么区别?何时使用它们?
any
和 unknown
都表示不确定的类型,但它们在类型安全性上有显著差异。
-
any
(不安全的任意类型) :any
表示你可以赋予它任何类型的值,并且可以对any
类型的值执行任何操作,跳过所有类型检查。- 使用
any
会丧失 TypeScript 提供的类型安全性,将代码退化为纯 JavaScript。
TypeScriptlet value: any = 'hello'; value = 123; // OK value.foo(); // OK,编译时不会报错,运行时可能报错 value[0]; // OK
-
unknown
(类型安全的任意类型) :unknown
同样可以接受任何类型的值。- 但与
any
不同,你不能直接对unknown
类型的值进行任何操作 (除了赋值给any
或unknown
类型),除非你先通过类型收窄 (Type Narrowing) 来确定它的具体类型。 unknown
强制你进行类型检查,从而提供更好的类型安全性。
TypeScriptlet 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) :
TypeScriptlet someValue: any = "this is a string"; let strLength: number = (<string>someValue).length;
-
as
语法 (As syntax) : 这是在 JSX 中推荐的语法,因为尖括号语法会与 JSX 标签冲突。TypeScriptlet someValue: any = "this is a string"; let strLength: number = (someValue as string).length;
何时使用类型断言:
- 当你明确知道某个值的运行时类型,但 TypeScript 编译器无法自动推断出来时。
- 将
any
或unknown
类型转换为更具体的类型:这是最常见的用法。 - 处理 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"
}
枚举的实现原理 (编译后):
-
数字枚举:会被编译成一个双向映射的对象,既可以从名称查值,也可以从值查名称(反向映射)。
tsvar 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 仍然会进行类型检查。
常见推断场景:
-
变量初始化:
TypeScriptlet name = "Alice"; // 推断为 string let age = 30; // 推断为 number let isActive = true; // 推断为 boolean
-
函数返回值:
TypeScriptfunction add(a: number, b: number) { return a + b; // 推断返回类型为 number }
-
数组:
TypeScriptlet numbers = [1, 2, 3]; // 推断为 number[] let mixed = [1, "hello"]; // 推断为 (number | string)[]
-
对象字面量:
TypeScriptlet user = { id: 1, name: "Bob" }; // 推断为 { id: number; name: string; }
何时会推断为 any
:
-
当你声明变量但没有初始化时:
TypeScriptlet 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、CommonJSmodule
: 模块系统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
: 在类型上下文中获取变量的类型typescriptinterface 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中,可空类型是指一个变量可以存储特定类型的值,也可以存储null
或undefined
。(通过使用可空类型,开发者可以明确表达一个变量可能包含特定类型的值,也可能不包含值(即为null
或undefined
)。这有助于提高代码的可读性,并使得变量的可能取值范围更加清晰明了)。
为了声明一个可空类型,可以使用联合类型(Union Types),例如 number | null
或 string | 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'));
在上面的示例中:
- 使用模块时,我们可以使用
export
和import
关键字来定义和引入模块中的函数或变量。 - 而在命名空间中,我们使用 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中使用const
和readonly
时,它们的行为有一些显著的区别:
-
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 类型是什么?
-
表示永远不会发生的值的类型
-
常用于抛出异常或无限循环的函数返回类型
typescriptfunction error(message: string): never { throw new Error(message); }