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 类型别名
| 特性 | Interface | Type |
|---|---|---|
| 声明合并 | ✅ 支持(同名接口自动合并) | ❌ 不支持 |
| 继承 | 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) // 安全访问
}
}