跳到主要内容

TypeScript 基础知识

问题

TypeScript 是什么?它与 JavaScript 有什么区别?为什么要使用 TypeScript?

答案

TypeScript 是 JavaScript 的超集,添加了静态类型系统和其他特性。它由 Microsoft 开发,最终编译为纯 JavaScript 运行。


TypeScript vs JavaScript

特性TypeScriptJavaScript
类型系统静态类型(编译时检查)动态类型(运行时检查)
编译需要编译为 JS直接运行
IDE 支持强大的智能提示有限
学习曲线较高较低
代码维护更易维护大型项目困难
执行环境浏览器/Node.js(编译后)浏览器/Node.js

基础类型

原始类型

// 布尔
const isDone: boolean = false;

// 数字(支持十进制、十六进制、二进制、八进制)
const decimal: number = 6;
const hex: number = 0xf00d;
const binary: number = 0b1010;

// 字符串
const name: string = 'TypeScript';
const greeting: string = `Hello, ${name}`;

// null 和 undefined
const n: null = null;
const u: undefined = undefined;

// Symbol
const sym: symbol = Symbol('key');

// BigInt
const big: bigint = 100n;

数组

// 两种写法等价
const numbers: number[] = [1, 2, 3];
const strings: Array<string> = ['a', 'b', 'c'];

// 只读数组
const readonlyArr: readonly number[] = [1, 2, 3];
const readonlyArr2: ReadonlyArray<number> = [1, 2, 3];

元组(Tuple)

固定长度和类型的数组:

// 定义元组类型
let tuple: [string, number] = ['hello', 10];

// 访问元素
const str: string = tuple[0];
const num: number = tuple[1];

// 可选元素
let optionalTuple: [string, number?] = ['hello'];

// 剩余元素
let restTuple: [string, ...number[]] = ['hello', 1, 2, 3];

// 命名元组(提高可读性)
type NamedTuple = [name: string, age: number];
const person: NamedTuple = ['Alice', 25];

枚举(Enum)

// 数字枚举(默认从 0 开始)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}

// 指定初始值
enum Status {
Pending = 1,
Active, // 2
Inactive // 3
}

// 字符串枚举
enum HttpMethod {
Get = 'GET',
Post = 'POST',
Put = 'PUT',
Delete = 'DELETE'
}

// 常量枚举(编译时内联)
const enum Color {
Red,
Green,
Blue
}
const red = Color.Red; // 编译为 const red = 0;

// 使用枚举
function move(direction: Direction): void {
console.log(direction);
}
move(Direction.Up);

any、unknown、never、void

// any - 任意类型,跳过类型检查
let anyValue: any = 4;
anyValue = 'string';
anyValue = false;
anyValue.foo.bar; // 不报错

// unknown - 安全的 any,使用前必须类型检查
let unknownValue: unknown = 4;
unknownValue = 'string';
// unknownValue.foo; // 错误:Object is of type 'unknown'
if (typeof unknownValue === 'string') {
console.log(unknownValue.toUpperCase()); // OK
}

// void - 无返回值
function log(message: string): void {
console.log(message);
}

// never - 永不返回(抛出异常或无限循环)
function throwError(message: string): never {
throw new Error(message);
}

function infiniteLoop(): never {
while (true) {}
}
any vs unknown
  • any 放弃类型检查,应尽量避免使用
  • unknown 是类型安全的,必须先收窄类型才能使用
  • 优先使用 unknown 替代 any

对象类型

对象字面量

// 内联类型
const user: { name: string; age: number } = {
name: 'Alice',
age: 25
};

// 可选属性
const config: { url: string; timeout?: number } = {
url: 'https://api.example.com'
};

// 只读属性
const point: { readonly x: number; readonly y: number } = {
x: 10,
y: 20
};
// point.x = 5; // 错误:Cannot assign to 'x' because it is a read-only property

// 索引签名
const dict: { [key: string]: number } = {
apple: 1,
banana: 2
};

Object、object、

// Object - 所有拥有 toString、hasOwnProperty 方法的类型
const obj1: Object = {}; // OK
const obj2: Object = []; // OK
const obj3: Object = () => {}; // OK

// object - 非原始类型(对象、数组、函数等)
const obj4: object = {}; // OK
const obj5: object = []; // OK
// const obj6: object = 'string'; // 错误

// {} - 空对象类型,可以是任何非 null/undefined 的值
const obj7: {} = {}; // OK
const obj8: {} = 'string'; // OK
// const obj9: {} = null; // 错误

函数类型

函数声明

// 函数声明
function add(x: number, y: number): number {
return x + y;
}

// 函数表达式
const multiply: (x: number, y: number) => number = (x, y) => x * y;

// 可选参数(必须在必选参数后面)
function greet(name: string, greeting?: string): string {
return `${greeting || 'Hello'}, ${name}!`;
}

// 默认参数
function createUser(name: string, age: number = 18): object {
return { name, age };
}

// 剩余参数
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}

函数重载

// 重载签名
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;

// 实现签名
function format(value: string | number | Date): string {
if (typeof value === 'string') {
return value.trim();
} else if (typeof value === 'number') {
return value.toFixed(2);
} else {
return value.toISOString();
}
}

// 使用
format('hello'); // OK
format(123); // OK
format(new Date()); // OK
// format(true); // 错误:没有匹配的重载

this 类型

interface User {
name: string;
greet(this: User): void;
}

const user: User = {
name: 'Alice',
greet() {
console.log(`Hello, ${this.name}`);
}
};

user.greet(); // OK

// const greet = user.greet;
// greet(); // 错误:The 'this' context of type 'void' is not assignable

类型别名与接口

类型别名(Type Alias)

// 基础类型别名
type ID = string | number;
type Callback = (data: string) => void;

// 对象类型别名
type User = {
id: ID;
name: string;
email: string;
};

// 泛型类型别名
type Response<T> = {
data: T;
status: number;
message: string;
};

// 使用
const userId: ID = 123;
const response: Response<User> = {
data: { id: 1, name: 'Alice', email: 'alice@example.com' },
status: 200,
message: 'OK'
};

接口(Interface)

// 定义接口
interface Person {
name: string;
age: number;
email?: string; // 可选属性
readonly id: number; // 只读属性
}

// 接口继承
interface Employee extends Person {
department: string;
salary: number;
}

// 多重继承
interface Manager extends Employee {
subordinates: Employee[];
}

// 接口合并(Declaration Merging)
interface User {
name: string;
}

interface User {
age: number;
}

// User 现在有 name 和 age 两个属性
const user: User = { name: 'Alice', age: 25 };

函数接口

// 可调用接口
interface SearchFunc {
(source: string, subString: string): boolean;
}

const search: SearchFunc = (source, subString) => {
return source.includes(subString);
};

// 带属性的可调用接口
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}

function createCounter(): Counter {
const counter = ((start: number) => start.toString()) as Counter;
counter.interval = 1000;
counter.reset = () => {};
return counter;
}

类(Class)

class Animal {
// 属性
public name: string;
protected age: number;
private _id: number;
readonly species: string;

// 静态属性
static count: number = 0;

// 构造函数
constructor(name: string, age: number, species: string) {
this.name = name;
this.age = age;
this._id = ++Animal.count;
this.species = species;
}

// 方法
public speak(): void {
console.log(`${this.name} makes a sound`);
}

// 访问器
get id(): number {
return this._id;
}

// 静态方法
static getCount(): number {
return Animal.count;
}
}

// 继承
class Dog extends Animal {
breed: string;

constructor(name: string, age: number, breed: string) {
super(name, age, 'Canine');
this.breed = breed;
}

// 方法重写
speak(): void {
console.log(`${this.name} barks`);
}
}

// 抽象类
abstract class Shape {
abstract getArea(): number;

printArea(): void {
console.log(`Area: ${this.getArea()}`);
}
}

class Circle extends Shape {
constructor(private radius: number) {
super();
}

getArea(): number {
return Math.PI * this.radius ** 2;
}
}

类实现接口

interface Printable {
print(): void;
}

interface Loggable {
log(message: string): void;
}

class Document implements Printable, Loggable {
print(): void {
console.log('Printing...');
}

log(message: string): void {
console.log(`Log: ${message}`);
}
}

模块系统

ES Modules

// utils.ts - 导出
export const PI = 3.14159;

export function add(a: number, b: number): number {
return a + b;
}

export default class Calculator {
// ...
}

// 类型导出
export type { User } from './types';
export interface Config {
// ...
}

// main.ts - 导入
import Calculator, { PI, add } from './utils';
import type { Config } from './utils';

// 重命名导入
import { add as addNumbers } from './utils';

// 命名空间导入
import * as Utils from './utils';
console.log(Utils.PI);

类型声明文件

// types.d.ts
declare module 'my-module' {
export function doSomething(): void;
export const version: string;
}

// 全局类型声明
declare global {
interface Window {
myCustomProperty: string;
}
}

// 扩展已有模块
declare module 'express' {
interface Request {
user?: {
id: string;
name: string;
};
}
}

常见面试问题

Q1: TypeScript 的优势是什么?

答案

  1. 编译时类型检查:在代码运行前发现错误
  2. 更好的 IDE 支持:智能提示、自动补全、重构
  3. 代码可读性:类型即文档,易于理解
  4. 可维护性:大型项目更易维护
  5. 渐进式采用:可逐步迁移,与 JS 兼容
// 类型检查示例
function processUser(user: { name: string; age: number }) {
// IDE 自动提示 user 的属性
console.log(user.name.toUpperCase());
// user.email; // 编译错误:属性 'email' 不存在
}

Q2: const 断言和 as const 的作用?

答案

as const 将值断言为字面量类型,使其变为只读且不可变:

// 不使用 as const
const config = {
url: 'https://api.example.com',
method: 'GET'
};
// config 类型:{ url: string; method: string }

// 使用 as const
const configConst = {
url: 'https://api.example.com',
method: 'GET'
} as const;
// configConst 类型:{ readonly url: "https://api.example.com"; readonly method: "GET" }

// 数组
const arr = [1, 2, 3] as const;
// arr 类型:readonly [1, 2, 3]

// 常用于创建枚举替代
const HttpMethods = ['GET', 'POST', 'PUT', 'DELETE'] as const;
type HttpMethod = typeof HttpMethods[number]; // "GET" | "POST" | "PUT" | "DELETE"

Q3: TypeScript 中的 ! 和 ? 的区别?

答案

符号名称作用
?可选链/可选属性表示属性可能不存在
!非空断言告诉编译器值一定存在
// ? 可选属性
interface User {
name: string;
email?: string; // 可选
}

// ? 可选链
const email = user?.email?.toLowerCase();

// ! 非空断言(告诉编译器这里一定有值)
const element = document.getElementById('app')!;
element.innerHTML = 'Hello';

// ! 明确赋值断言
class Example {
name!: string; // 告诉编译器会在其他地方初始化

initialize() {
this.name = 'initialized';
}
}
注意

! 非空断言会跳过类型检查,如果值实际为 null/undefined 会导致运行时错误。应谨慎使用,优先使用类型守卫。

Q4: declare 关键字的作用?

答案

declare 用于声明已存在于其他地方的变量、函数、类等,不会生成 JavaScript 代码:

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

// 声明模块
declare module 'lodash' {
export function chunk<T>(array: T[], size: number): T[][];
}

// 声明全局类型
declare global {
interface Window {
analytics: {
track(event: string): void;
};
}
}

// 声明文件 .d.ts 中常用
// 告诉 TypeScript 这些类型存在,但不需要编译器生成代码

Q5: any、unknown、never 的区别和使用场景

答案

这三个类型分别代表 TypeScript 类型系统的三个极端:最宽松(any)、最安全的宽松(unknown)、最严格(never)。

类型含义可赋值给可被赋值能否直接操作
any任意类型,关闭类型检查任何类型接受任何值可以(不安全)
unknown未知类型,安全的 any只能赋给 unknown/any接受任何值不行,必须先收窄
never不存在的类型可赋给任何类型不接受任何值不适用
// ===== any =====
// 放弃类型检查,任何操作都不报错
let a: any = 'hello';
a.foo.bar.baz(); // 不报错,但运行时爆炸
a = 42;
a = true;

// ===== unknown =====
// 类型安全的 any,必须先收窄才能使用
let u: unknown = 'hello';
// u.toUpperCase(); // ❌ 错误:Object is of type 'unknown'

// 必须先进行类型收窄
if (typeof u === 'string') {
u.toUpperCase(); // ✅ OK
}

// ===== never =====
// 表示永远不会出现的值
function throwError(msg: string): never {
throw new Error(msg);
}

function infiniteLoop(): never {
while (true) {}
}

never 的高级用途:穷举检查

type Shape = 'circle' | 'square' | 'triangle';

function getArea(shape: Shape): number {
switch (shape) {
case 'circle':
return Math.PI * 10 ** 2;
case 'square':
return 10 * 10;
case 'triangle':
return (10 * 10) / 2;
default:
const _exhaustive: never = shape; // 如果漏掉某个 case,这里会报错
return _exhaustive;
}
}

// 假设后续新增了 'rectangle',但忘了加 case
// type Shape = 'circle' | 'square' | 'triangle' | 'rectangle';
// default 中 shape 类型变为 'rectangle',不能赋给 never,编译报错!
使用建议
  1. 避免 any:每个 any 都是类型安全的漏洞,用 unknown 替代
  2. unknown 用于外部输入:API 响应、JSON.parse 结果、第三方数据
  3. never 用于穷举检查:确保 switch/if 覆盖了所有情况
  4. ESLint 规则 @typescript-eslint/no-explicit-any 可以禁止使用 any

Q6: 枚举(enum)vs 联合类型 vs const 对象,如何选择?

答案

这三种方式都能表示一组固定的常量值,但适用场景不同。

1. 枚举(enum)

// 数字枚举
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}

// 字符串枚举
enum HttpStatus {
OK = 'OK',
NotFound = 'NOT_FOUND',
ServerError = 'SERVER_ERROR',
}

// 使用
function move(dir: Direction): void {
console.log(dir);
}
move(Direction.Up);

// ⚠️ 问题:数字枚举允许反向映射和任意数字赋值
const d: Direction = 999; // 不报错!这是个安全漏洞

2. 联合类型(推荐用于大多数场景)

type Direction = 'up' | 'down' | 'left' | 'right';
type HttpStatus = 200 | 404 | 500;

function move(dir: Direction): void {
console.log(dir);
}

move('up'); // ✅
// move('diagonal'); // ❌ 编译错误

// 优点:编译后完全消失,零运行时开销
// 优点:类型安全,不会有数字枚举的漏洞

3. const 对象 + as const

const Direction = {
Up: 'up',
Down: 'down',
Left: 'left',
Right: 'right',
} as const;

type Direction = typeof Direction[keyof typeof Direction];
// 'up' | 'down' | 'left' | 'right'

// 好处:既有运行时对象可遍历,又有类型安全
console.log(Object.values(Direction)); // ['up', 'down', 'left', 'right']

function move(dir: Direction): void {
console.log(dir);
}
move(Direction.Up); // ✅ 可以用对象属性访问
move('up'); // ✅ 也可以直接用字面量

对比总结

特性enum联合类型const 对象
运行时存在是(生成代码)否(零开销)是(普通对象)
可遍历是(有坑)
Tree Shaking差(const enum 除外)
类型安全数字枚举有漏洞严格严格
IDE 提示
反向映射数字枚举支持不支持不支持
选择建议
  • 大多数场景:用联合类型,零开销、类型安全
  • 需要运行时遍历值:用 const 对象 + as const
  • 需要反向映射:用 enum(较少见)
  • 与后端枚举对应:用 const 对象字符串 enum
  • 尽量避免数字枚举:安全漏洞多,用 const enum 可以缓解但仍有限制

Q7: TypeScript 的结构类型系统和名义类型系统有什么区别?

答案

这是理解 TypeScript 类型系统的核心概念。TypeScript 采用结构类型系统(Structural Type System),也叫"鸭子类型"——只要形状匹配就算同一类型,不管类型名是否相同。

结构类型(TypeScript 的方式)

interface Cat {
name: string;
meow(): void;
}

interface Dog {
name: string;
meow(): void; // 恰好也有 meow 方法
}

const cat: Cat = { name: 'Tom', meow() {} };
const dog: Dog = cat; // ✅ 不报错!因为结构兼容

// Cat 和 Dog 名字不同,但结构一样,所以互相兼容

名义类型(Java/C# 的方式)

// 在名义类型系统中,即使结构完全相同,类型名不同就不兼容
// Cat 和 Dog 是不同的类型,不能互相赋值
// TypeScript 原生不支持名义类型,但可以模拟

结构类型带来的问题

// 问题:不同含义的 ID 可以互相赋值
type UserId = string;
type OrderId = string;

function getUser(id: UserId): void { /* ... */ }

const orderId: OrderId = 'order-123';
getUser(orderId); // ✅ 不报错!但逻辑上是错误的

模拟名义类型(品牌类型 / Branded Types)

// 通过添加一个不存在的品牌属性,让结构类型具有名义特性
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

// 创建品牌类型的值
function createUserId(id: string): UserId {
return id as UserId;
}

function createOrderId(id: string): OrderId {
return id as OrderId;
}

function getUser(id: UserId): void {
console.log('Getting user:', id);
}

const userId = createUserId('user-123');
const orderId = createOrderId('order-456');

getUser(userId); // ✅ OK
// getUser(orderId); // ❌ 编译错误:OrderId 不能赋给 UserId

// 实际应用:货币类型安全
type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;

function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}

const dollars = 100 as USD;
const euros = 85 as EUR;
addUSD(dollars, dollars); // ✅
// addUSD(dollars, euros); // ❌ 不能把欧元当美元用
结构类型的优势

结构类型系统使得 TypeScript 与 JavaScript 生态高度兼容。JavaScript 天然是鸭子类型的,结构类型系统让现有 JS 代码更容易迁移到 TS。

Q8: 如何在项目中渐进式引入 TypeScript?

答案

渐进式引入 TypeScript 是大多数存量项目的最佳策略,核心原则是不影响现有功能、逐步收紧类型检查

第 1 步:初始化配置(宽松模式)

tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"outDir": "./dist",

// 关键:初期设为宽松,后续逐步收紧
"strict": false,
"allowJs": true, // 允许 JS 文件
"checkJs": false, // 不检查 JS 文件
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src"]
}

第 2 步:JS 文件加类型注释(无需改后缀)

src/utils.js
// @ts-check  <-- 加这一行就能启用类型检查

/** @type {(a: number, b: number) => number} */
function add(a, b) {
return a + b;
}

/** @typedef {{ name: string; age: number }} User */

/** @param {User} user */
function greet(user) {
return `Hello, ${user.name}`;
}

第 3 步:逐文件改后缀 .js -> .ts

从以下文件开始迁移(风险低、收益高):

  1. 工具函数 (utils.ts) - 纯函数,无副作用
  2. 类型定义 (types.ts) - 先定义好公共类型
  3. 新文件 - 新写的文件直接用 .ts
  4. 核心模块 - 最后迁移复杂的业务模块
src/types/index.ts
// 第一步:先把公共类型定义好
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}

export interface ApiResponse<T> {
code: number;
data: T;
message: string;
}

第 4 步:逐步开启严格选项

tsconfig.json - 逐步收紧
{
"compilerOptions": {
// 第一阶段:开启这两个
"noImplicitAny": true, // 禁止隐式 any
"strictNullChecks": true, // 严格空值检查

// 第二阶段
"strictFunctionTypes": true, // 严格函数类型
"strictBindCallApply": true, // 严格 bind/call/apply

// 第三阶段
"noImplicitReturns": true, // 禁止隐式返回
"noFallthroughCasesInSwitch": true,

// 最终目标:开启 strict
// "strict": true // 等同于开启所有严格选项
}
}

第 5 步:处理第三方库

npm install --save-dev @types/react @types/node @types/lodash
src/types/declarations.d.ts
// 没有类型定义的第三方库,先用声明文件兜底
declare module 'legacy-lib' {
const lib: any;
export default lib;
}

// 后续可以逐步完善类型
declare module 'legacy-lib' {
export function doSomething(input: string): number;
}

迁移策略总结

迁移注意事项
  1. 不要一次性全改:大规模重命名文件可能引发大量编译错误
  2. 配合 CI 检查:在 CI 中运行 tsc --noEmit 确保不引入新的类型错误
  3. // @ts-expect-error 暂时跳过:比 any 好,后续容易搜索和修复
  4. 团队达成共识:制定迁移计划,确定每个阶段的目标和时间线

相关链接