Skip to content

TypeScript 核心概念

大白话解释: TypeScript 就是"严格的 JavaScript"。普通 JS 像是用铅笔写字——想写啥写啥,写错了运行时才知道;TS 像是用钢笔写字——下笔前先想清楚类型,写错了编辑器立刻告诉你。

为什么要用 TypeScript?

  • 智能提示:编辑器知道变量是什么类型,输入 . 自动弹出可用属性
  • 提前报错:拼写错误、类型错误在写代码时就发现,不用等运行时
  • 重构安全:改了接口,所有用到的地方都会报错,不会遗漏
  • 代码即文档:类型就是最好的注释,看类型就知道函数干嘛的

TypeScript 是 JavaScript 的超集,添加了静态类型系统和其他特性,提供更好的开发体验和代码质量。


基础类型

原始类型

ts
// ---- 原始类型:最基本的数据类型 ----

// string:字符串,用引号包裹的文字
let name: string = '张三'

// number:数字,整数和小数都是 number
let age: number = 25
let price: number = 9.99

// boolean:布尔值,只有 true 和 false 两个值
let isActive: boolean = true

// null:空,表示"故意没有值"
let nothing: null = null

// undefined:未定义,表示"还没赋值"
let notDefined: undefined = undefined

// symbol:唯一标识符,常用于对象的私有属性键
let id: symbol = Symbol('id')

// bigint:大整数,处理超过 Number.MAX_SAFE_INTEGER 的数字
let bigNum: bigint = 100n

数组和元组

ts
// ---- 数组:一组相同类型的数据 ----

// 写法一:类型[]
let numbers: number[] = [1, 2, 3]

// 写法二:Array<类型>(泛型写法)
let strings: Array<string> = ['a', 'b', 'c']

// 混合类型数组(联合类型)
let mixed: (string | number)[] = [1, 'two', 3]

// ---- 元组:固定长度、固定类型的数组 ----
// 大白话:就像一个"固定格子的收纳盒",每个格子放什么类型是定好的

// 基本元组
let tuple: [string, number] = ['张三', 25]
// tuple = [25, '张三']  // ❌ 顺序错了,类型不匹配

// 命名元组(给每个位置起名字,更好理解)
let namedTuple: [name: string, age: number] = ['李四', 30]

// 只读数组:创建后不能修改
let readonlyArr: readonly number[] = [1, 2, 3]
// readonlyArr.push(4)  // ❌ 错误,只读数组不能修改

函数类型

ts
// ---- 函数类型:定义函数的参数和返回值类型 ----
// 大白话:就像"快递单",规定了寄件人要提供什么(参数),收件人能收到什么(返回值)

// 基本函数类型
function add(a: number, b: number): number {
  return a + b
}

// 箭头函数
const multiply = (a: number, b: number): number => a * b

// 可选参数:加 ? 表示可以不传
function greet(name: string, age?: number): string {
  if (age) {
    return `你好,我是${name},今年${age}岁`
  }
  return `你好,我是${name}`
}
greet('张三')      // ✅
greet('张三', 25)  // ✅

// 默认参数:给参数一个默认值
interface User {
  name: string
  role: string
}
function createUser(name: string, role: string = 'user'): User {
  return { name, role }
}
createUser('张三')           // role = 'user'
createUser('张三', 'admin')  // role = 'admin'

// 剩余参数:收集多余的参数到数组
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0)
}
sum(1, 2, 3)      // 6
sum(1, 2, 3, 4)   // 10

// 函数重载:同一个函数,不同的参数类型,不同的返回值
// 什么时候用:当一个函数需要根据参数类型返回不同结果时
// 实际场景:表单验证(根据字段类型用不同规则)、数据格式化(数字保留小数、日期转字符串)

// 重载签名(声明部分,告诉 TS 有哪些调用方式)
function format(value: string): string      // 传字符串 → 返回大写
function format(value: number): string      // 传数字 → 返回两位小数
function format(value: Date): string        // 传日期 → 返回 ISO 字符串

// 实现签名(实际逻辑,参数类型是联合类型)
function format(value: string | number | Date): string {
  if (typeof value === 'string') {
    return value.toUpperCase()
  } else if (typeof value === 'number') {
    return value.toFixed(2)
  } else {
    return value.toISOString()
  }
}

format('hello')              // ✅ 返回 'HELLO'
format(3.14159)              // ✅ 返回 '3.14'
format(new Date())           // ✅ 返回 '2024-01-22T...'
// format(true)              // ❌ 没有匹配的重载签名

枚举

ts
// ---- 枚举:给一组数字或字符串起个名字 ----
// 大白话:就像"菜单选项",把一堆魔法数字/字符串变成有意义的名字

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

// 字符串枚举(每个值都是字符串,更直观)
enum Status {
  Active = 'ACTIVE',
  Inactive = 'INACTIVE',
  Pending = 'PENDING',
}
let status: Status = Status.Active  // 'ACTIVE'

// 常量枚举(编译后被内联,性能更好,但不能遍历)
// ⚠️ 注意:如果 tsconfig 开启了 isolatedModules(如 Vue + Vite 项目),
// const enum 会报错,因为它要求跨文件内联。此时应改用普通 enum 或字面量联合类型。
const enum Color {
  Red = 'RED',
  Green = 'GREEN',
  Blue = 'BLUE',
}
let color: Color = Color.Red  // 编译后直接变成 'RED'

// 实际用法:用枚举替代魔法数字
enum HttpStatus {
  OK = 200,
  NotFound = 404,
  ServerError = 500,
}

function handleResponse(code: HttpStatus) {
  if (code === HttpStatus.OK) {
    console.log('请求成功')
  } else if (code === HttpStatus.NotFound) {
    console.log('页面不存在')
  }
}

特殊类型

ts
// ---- 特殊类型:TS 里的"逃生舱" ----

// any:任意类型,关闭类型检查(尽量少用)
// 大白话:告诉 TS "别管我,我自己负责"
let anything: any = 42
anything = 'hello'  // ✅ 不报错
anything = true     // ✅ 不报错
anything.foo.bar    // ✅ 不报错,但运行时可能炸

// unknown:安全的 any(使用前必须检查类型)
// 大白话:告诉 TS "我还不知道是什么类型,用的时候我会先检查"
let value: unknown = 42
// value.toFixed(2)  // ❌ 错误!不能直接用
if (typeof value === 'number') {
  value.toFixed(2)  // ✅ 类型收窄后可用
}

// void:函数无返回值
// 大白话:这个函数只是做事,不返回任何东西
function log(message: string): void {
  console.log(message)
  // 没有 return 语句,或者 return;
}

// never:永远不会返回的函数
// 大白话:这个函数要么死循环,要么抛异常,反正不会正常结束
function throwError(message: string): never {
  throw new Error(message)
}

function infiniteLoop(): never {
  while (true) {
    // 永远不会结束
  }
}

// object:非原始类型(不包括 number、string、boolean 等)
let obj: object = { name: '张三' }
// obj.name  // ❌ 错误,object 类型不能直接访问属性

// 实际开发中,用具体接口代替 object
interface User {
  name: string
  age: number
}
let user: User = { name: '张三', age: 25 }
user.name  // ✅ 可以访问

接口和类型别名

接口(Interface)

ts
// ---- 接口:定义对象的"形状" ----
// 大白话:就像"合同模板",规定了对象必须有哪些属性、什么类型

// 基本接口
interface User {
  name: string           // 必需属性
  age: number            // 必需属性
  email?: string         // 可选属性(加 ? 表示可以没有)
  readonly id: number    // 只读属性(创建后不能修改)
}

// 使用接口
const user: User = {
  id: 1,
  name: '张三',
  age: 25,
  // email 可以省略,因为是可选的
}
// user.id = 2  // ❌ 错误,只读属性不能修改

// 接口继承:一个接口可以继承另一个接口
// 大白话:Admin 是 User 的"升级版",拥有 User 的所有属性,外加自己的
interface Admin extends User {
  permissions: string[]
}

const admin: Admin = {
  id: 1,
  name: '管理员',
  age: 30,
  permissions: ['read', 'write', 'delete'],
}

// 函数接口:定义函数的类型
interface SearchFunc {
  (keyword: string, page: number): Promise<User[]>
}

const search: SearchFunc = async (keyword, page) => {
  // 实现搜索逻辑
  return []
}

// 索引签名:允许任意数量的属性
// 大白话:就像一个"动态字典",key 和 value 的类型是固定的,但数量不限
interface Dictionary {
  [key: string]: string
}

const dict: Dictionary = {
  name: '张三',
  age: '25',  // 注意:value 必须是 string
}

类型别名(Type)

ts
// ---- 类型别名:给类型起个名字 ----
// 大白话:就像给复杂的类型取个"外号",用起来更方便

// 基本类型别名
type ID = string | number
type Status = 'active' | 'inactive' | 'pending'

// 对象类型别名
type Point = {
  x: number
  y: number
}

// 联合类型:多个类型中选一个
// 大白话:就像"多选一",Result 要么是 Success 要么是 Error
type Success = { status: 'success'; data: any }
type Failure = { status: 'error'; message: string }
type Result = Success | Failure

// 交叉类型:合并多个类型
type ExtendedUser = User & {
  role: string
}

// 条件类型:根据条件决定类型
type IsString<T> = T extends string ? true : false
type A = IsString<string>  // true
type B = IsString<number>  // false

// 实际用法:定义 API 响应类型
type ApiResponse<T> = {
  code: number
  message: string
  data: T
}

type UserResponse = ApiResponse<User>
type UserListResponse = ApiResponse<User[]>

接口 vs 类型别名

特性InterfaceType
声明合并✅ 支持(同名接口自动合并)❌ 不支持
继承extends& 交叉
联合类型❌ 不支持✅ 支持
基本类型别名❌ 不支持✅ 支持
计算属性❌ 不支持✅ 支持

选择建议:

  • 定义对象结构 → 用 Interface(支持声明合并,性能更好)
  • 联合/交叉类型 → 用 Type(接口不支持)
  • 基本类型别名 → 用 Type(接口不能定义基本类型)
  • 工具类型 → 用 Type(支持条件类型和映射类型)

泛型

详细内容请查看 TypeScript 泛型

ts
// ---- 泛型:让函数/类支持多种类型 ----
// 大白话:就像"万能遥控器",不管什么品牌(类型)的电视(数据),都能用

// 基本泛型函数
function identity<T>(arg: T): T {
  return arg
}
const num = identity<number>(42)  // T = number
const str = identity('hello')     // T = string(自动推断)

// 泛型接口
interface ApiResponse<T> {
  code: number
  message: string
  data: T
}

// 泛型约束:限制 T 必须满足某些条件
function logLength<T extends { length: number }>(arg: T): T {
  console.log(arg.length)
  return arg
}

类型收窄

类型守卫

ts
// ---- 类型守卫:在运行时检查类型 ----
// 大白话:就像"安检",先检查是什么类型,再决定怎么处理

// typeof 守卫:检查基本类型
function format(value: string | number) {
  if (typeof value === 'string') {
    return value.toUpperCase()  // 这里 TS 知道 value 是 string
  } else {
    return value.toFixed(2)     // 这里 TS 知道 value 是 number
  }
}

// instanceof 守卫:检查类实例
function formatDate(value: string | Date) {
  if (value instanceof Date) {
    return value.toISOString()  // 这里 TS 知道 value 是 Date
  } else {
    return new Date(value).toISOString()  // 这里 TS 知道 value 是 string
  }
}

// in 守卫:检查属性是否存在
interface Circle {
  kind: 'circle'
  radius: number
}

interface Square {
  kind: 'square'
  side: number
}

type Shape = Circle | Square

function getArea(shape: Shape) {
  if ('radius' in shape) {
    return Math.PI * shape.radius ** 2  // 这里 TS 知道 shape 是 Circle
  } else {
    return shape.side ** 2  // 这里 TS 知道 shape 是 Square
  }
}

可辨识联合

ts
// ---- 可辨识联合:用字面量类型区分不同类型 ----
// 大白话:就像"快递单上的类型栏",根据类型栏的值,就知道怎么处理

interface Circle {
  kind: 'circle'  // 这个 kind 就是"辨识字段"
  radius: number
}

interface Square {
  kind: 'square'
  side: number
}

interface Triangle {
  kind: 'triangle'
  base: number
  height: number
}

type Shape = Circle | Square | Triangle

// 穷尽检查:确保处理了所有可能的类型
function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'square':
      return shape.side ** 2
    case 'triangle':
      return (shape.base * shape.height) / 2
    default:
      // 如果遗漏了某个 case,这里会报错
      // 这是 TS 的"穷尽检查",确保你处理了所有情况
      const _exhaustive: never = shape
      return _exhaustive
  }
}

// 实际用法:处理不同的 API 响应
interface SuccessResponse {
  status: 'success'
  data: any
}

interface ErrorResponse {
  status: 'error'
  message: string
  code: number
}

interface LoadingResponse {
  status: 'loading'
}

type ApiResponse = SuccessResponse | ErrorResponse | LoadingResponse

function handleResponse(response: ApiResponse) {
  switch (response.status) {
    case 'success':
      console.log('数据:', response.data)
      break
    case 'error':
      console.log('错误:', response.message)
      break
    case 'loading':
      console.log('加载中...')
      break
  }
}

模块和命名空间

ES 模块

ts
// ---- ES 模块:现代 JavaScript 的模块系统 ----
// 大白话:就像"文件柜",每个文件是一个抽屉,通过 import/export 打开抽屉拿东西

// 导出:把东西从抽屉里拿出来
export interface User {
  name: string
  age: number
}

export function createUser(name: string, age: number): User {
  return { name, age }
}

export default class UserService {
  // 默认导出:一个文件只能有一个
}

// 导入:从别的抽屉拿东西进来
import UserService, { User, createUser } from './user'
import type { User } from './user'  // 仅导入类型(编译后会被删除)

命名空间(不推荐,推荐使用 ES 模块)

ts
// ---- 命名空间:旧式的模块系统 ----
// 大白话:就像"文件夹",把相关的类型放在一起
// 注意:现在推荐用 ES 模块,命名空间主要用于声明文件

namespace Validation {
  export interface Validator {
    validate(value: string): boolean
  }

  export class EmailValidator implements Validator {
    validate(value: string): boolean {
      return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
    }
  }
}

// 使用
const validator = new Validation.EmailValidator()
validator.validate('[email protected]')

装饰器

注意: 装饰器有两种语法:

  • 新语法(TC39 Stage 3):不需要额外配置,参数是 (target, context),context 是 ClassDecoratorContext
  • 旧语法(experimental):需要在 tsconfig.json 中开启 "experimentalDecorators": true,参数是 (target, key, descriptor)

Vue 3 + Vite 项目默认使用新语法。如果你的项目使用旧语法(如 NestJS),需要开启配置。

类装饰器

ts
// ---- 装饰器:给类/方法/属性"贴标签" ----
// 大白话:就像"手机壳",不改变手机本身,但给手机加了新功能

// 类装饰器:给类添加额外功能
// 使用 TC39 新语法,参数是 (target, context)
// target: 被装饰的类本身
// context: ClassDecoratorContext 对象,包含类名、元数据等信息
function Logger<T extends new (...args: any[]) => any>(
  target: T,                          // 被装饰的类
  context: ClassDecoratorContext      // 装饰器上下文(新语法特有)
) {
  // 返回一个新的子类,覆盖原类
  return class extends target {
    constructor(...args: any[]) {
      super(...args)
      // context.name 是类名,新语法自动提供
      console.log(`Creating instance of ${context.name}`)
    }
  }
}

@Logger  // 给 Person 类"贴上" Logger 标签
class Person {
  constructor(public name: string) {}
}

// 使用时会自动打印日志
const p = new Person('张三')  // Creating instance of Person

方法装饰器

ts
// 方法装饰器:给方法添加额外功能
// 使用 TC39 新语法,参数是 (target, context)
// target: 原始方法函数
// context: ClassMethodDecoratorContext 对象,包含方法名等信息
function Log(
  target: Function,                    // 被装饰的方法
  context: ClassMethodDecoratorContext // 装饰器上下文
) {
  // 返回一个新方法,替换原方法
  return function (this: any, ...args: any[]) {
    console.log(`Calling ${String(context.name)} with`, args)
    // target.apply 调用原方法,this 绑定到当前实例
    return target.apply(this, args)
  }
}

class Calculator {
  @Log  // 给 add 方法"贴上" Log 标签
  add(a: number, b: number) {
    return a + b
  }
}

const calc = new Calculator()
calc.add(1, 2)  // Calling add with [1, 2]

配置文件(tsconfig.json)

基础配置

json
{
  "compilerOptions": {
    "target": "ES2020",           // 编译目标:ES2020 支持最新语法
    "module": "ESNext",           // 模块系统:ESNext 支持 import/export
    "lib": ["ES2020", "DOM", "DOM.Iterable"],  // 类型库:包含哪些 API 的类型
    "outDir": "./dist",           // 输出目录
    "rootDir": "./src",           // 源码目录
    "strict": true,               // 严格模式:开启所有严格检查(推荐)
    "esModuleInterop": true,      // ES 模块互操作:让 CommonJS 模块可以 default import
    "skipLibCheck": true,         // 跳过库检查:不检查 .d.ts 文件(加快编译)
    "forceConsistentCasingInFileNames": true,  // 强制文件名大小写一致
    "resolveJsonModule": true,    // 允许导入 JSON 文件
    "declaration": true,          // 生成 .d.ts 声明文件
    "declarationMap": true,       // 生成声明文件的 source map
    "sourceMap": true,            // 生成 source map(方便调试)
    "moduleResolution": "node",   // 模块解析策略:node 风格
    "allowSyntheticDefaultImports": true,  // 允许合成默认导入
    // 以下两项仅在使用旧语法装饰器时需要(如 NestJS);
    // Vue 3 + Vite 项目使用 TC39 新语法,不需要这两项
    "experimentalDecorators": true,        // 启用装饰器(旧语法,如 NestJS)。Vue 3 + Vite 使用新语法时不需要此项
    "emitDecoratorMetadata": true          // 发射装饰器元数据(旧语法配套,新语法不需要)(旧语法)
  },
  "include": ["src/**/*"],        // 包含哪些文件
  "exclude": ["node_modules", "dist"]  // 排除哪些文件
}

Vue 3 + Vite 项目配置

json
{
  "compilerOptions": {
    "target": "ESNext",            // Vite 项目用 ESNext,浏览器原生支持
    "module": "ESNext",            // 模块系统用 ESNext
    "moduleResolution": "bundler", // Vite 专用解析策略,比 node 更快
    "strict": true,                // 开启所有严格检查
    "jsx": "preserve",             // JSX 保留给 Vue 编译器处理
    "resolveJsonModule": true,     // 允许导入 JSON 文件
    "isolatedModules": true,       // 每个文件独立编译(Vite 要求)
    "esModuleInterop": true,       // ES 模块互操作
    "lib": ["ESNext", "DOM", "DOM.Iterable"],  // 浏览器 API 类型
    "skipLibCheck": true,          // 跳过 .d.ts 检查(加快编译)
    "noEmit": true,                // Vite 自己处理编译,TS 只做类型检查
    "baseUrl": ".",                // 路径别名基准目录
    "paths": {
      "@/*": ["src/*"]             // @ 指向 src 目录
    },
    "types": ["vite/client"]       // Vite 客户端类型(import.meta.env 等)
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"],  // 包含 .vue 文件
  "exclude": ["node_modules", "dist"]
}

常见问题

类型断言 vs 类型守卫

ts
// 类型断言:告诉 TS "我比你更清楚这个类型"(不安全)
const value1 = someValue as string
// 如果 someValue 实际不是 string,运行时会出问题

// 类型守卫:运行时检查类型(安全)
if (typeof someValue === 'string') {
  someValue.toUpperCase()  // 安全,因为检查过了
}

处理第三方库没有类型定义

ts
// 1. 安装类型定义(推荐)
// npm install --save-dev @types/lodash

// 2. 创建自定义声明文件
// types/lodash.d.ts
declare module 'lodash' {
  export function debounce<T extends (...args: any[]) => any>(
    func: T,
    wait?: number
  ): T
}

// 3. 使用 any(不推荐,但有时候没办法)
declare module 'some-lib' {
  const someLib: any
  export default someLib
}

什么时候用 any,什么时候用 unknown

ts
// 用 any:当你确实不在乎类型,或者在快速原型开发
// 用 unknown:当你需要接收外部数据,但会先检查类型

// ❌ 不推荐:直接用 any
function process(data: any) {
  data.foo.bar  // 不报错,但运行时可能炸
}

// ✅ 推荐:用 unknown + 类型守卫
interface HasFoo {
  foo: { bar: string }
}
function process(data: unknown) {
  if (typeof data === 'object' && data !== null && 'foo' in data) {
    const obj = data as HasFoo
    console.log(obj.foo.bar)  // 安全访问
  }
}

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准