TypeScript 基础知识
问题
TypeScript 是什么?它与 JavaScript 有什么区别?为什么要使用 TypeScript?
答案
TypeScript 是 JavaScript 的超集,添加了静态类型系统和其他特性。它由 Microsoft 开发,最终编译为纯 JavaScript 运行。
TypeScript vs JavaScript
| 特性 | TypeScript | JavaScript |
|---|---|---|
| 类型系统 | 静态类型(编译时检查) | 动态类型(运行时检查) |
| 编译 | 需要编译为 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放弃类型检查,应尽量避免使用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 的优势是什么?
答案:
- 编译时类型检查:在代码运行前发现错误
- 更好的 IDE 支持:智能提示、自动补全、重构
- 代码可读性:类型即文档,易于理解
- 可维护性:大型项目更易维护
- 渐进式采用:可逐步迁移,与 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,编译报错!
- 避免 any:每个
any都是类型安全的漏洞,用unknown替代 - unknown 用于外部输入:API 响应、
JSON.parse结果、第三方数据 - never 用于穷举检查:确保 switch/if 覆盖了所有情况
- 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 步:初始化配置(宽松模式)
{
"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 文件加类型注释(无需改后缀)
// @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
从以下文件开始迁移(风险低、收益高):
- 工具函数 (
utils.ts) - 纯函数,无副作用 - 类型定义 (
types.ts) - 先定义好公共类型 - 新文件 - 新写的文件直接用
.ts - 核心模块 - 最后迁移复杂的业务模块
// 第一步:先把公共类型定义好
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
export interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
第 4 步:逐步开启严格选项
{
"compilerOptions": {
// 第一阶段:开启这两个
"noImplicitAny": true, // 禁止隐式 any
"strictNullChecks": true, // 严格空值检查
// 第二阶段
"strictFunctionTypes": true, // 严格函数类型
"strictBindCallApply": true, // 严格 bind/call/apply
// 第三阶段
"noImplicitReturns": true, // 禁止隐式返回
"noFallthroughCasesInSwitch": true,
// 最终目标:开启 strict
// "strict": true // 等同于开启所有严格选项
}
}
第 5 步:处理第三方库
- npm
- Yarn
- pnpm
- Bun
npm install --save-dev @types/react @types/node @types/lodash
yarn add --dev @types/react @types/node @types/lodash
pnpm add --save-dev @types/react @types/node @types/lodash
bun add --dev @types/react @types/node @types/lodash
// 没有类型定义的第三方库,先用声明文件兜底
declare module 'legacy-lib' {
const lib: any;
export default lib;
}
// 后续可以逐步完善类型
declare module 'legacy-lib' {
export function doSomething(input: string): number;
}
迁移策略总结
- 不要一次性全改:大规模重命名文件可能引发大量编译错误
- 配合 CI 检查:在 CI 中运行
tsc --noEmit确保不引入新的类型错误 - 用
// @ts-expect-error暂时跳过:比any好,后续容易搜索和修复 - 团队达成共识:制定迁移计划,确定每个阶段的目标和时间线