TypeScript 基础:从 Java 视角看类型系统
为什么要学 TypeScript
JS 本身是动态弱类型语言,写大项目时"拼写错个属性名运行时才炸"是家常便饭。TypeScript 在编译时做类型检查,编译完再扔掉类型,产出的还是普通 JS。
一句话类比:TS 之于 JS,就像 Java 之于 Groovy —— 加上类型后,IDE 能准确补全、重构能批量改、单测能少写一半。
Vue 3 源码、Element Plus、VueUse 全部用 TS 写。学 Vue 的路上必然要过 TS 这一关。
1. 基础类型
let name: string = 'foo'
let age: number = 18
let active: boolean = true
let nothing: null = null
let notDefined: undefined = undefined
// 数组两种写法等价
let nums: number[] = [1, 2, 3]
let nums2: Array<number> = [1, 2, 3]
// 元组:固定长度和类型的数组
let pair: [string, number] = ['foo', 18]
Java 对照:
| Java | TS |
|---|---|
String |
string |
int / double |
number(JS 只有一种数字类型) |
boolean |
boolean |
null |
null |
Object 未初始化 |
undefined |
int[] |
number[] |
Pair<String, Integer> |
[string, number] |
注意:
- TS 类型用小写
string / number / boolean。大写的String / Number / Boolean是包装类型,几乎不用 - JS 没有
int / long / float / double之分,只有一个number(IEEE 754 双精度浮点)
2. 类型推导:大多数时候不用写类型
// 不用写 : string,TS 自己能推出来
let name = 'foo'
// 函数返回值也能推导
function add(a: number, b: number) {
return a + b // TS 推断返回 number
}
Java 对照:类似 Java 10+ 的 var。
建议:变量赋值时尽量省略类型标注,让 TS 自己推。只在函数参数、API 边界、公共类型上显式写类型。
3. 联合类型:一个值可能是多种类型之一
这是 TS 和 Java 最不一样的点之一。
let id: string | number
id = 'abc'
id = 123 // 都合法
// 常用于函数参数
function format(input: string | number) {
if (typeof input === 'string') {
return input.toUpperCase() // 这里 TS 知道 input 是 string
}
return input.toFixed(2) // 这里 TS 知道 input 是 number
}
Java 对照:Java 没有原生联合类型。类似场景要么用 Object 外加 instanceof 判断,要么用泛型 + 多个重载方法。TS 的联合类型优雅得多。
typeof 窄化:TS 能根据 typeof 自动推断出在 if 块里的具体类型,叫"类型守卫"。
4. 接口(interface):给对象定形状
interface User {
id: number
name: string
email?: string // 可选属性(? 表示可以不传)
readonly createdAt: Date // 只读属性
}
const u: User = {
id: 1,
name: 'foo',
createdAt: new Date(),
}
u.createdAt = new Date() // ❌ readonly,改不了
Java 对照:interface User 和 Java 的接口类似,但更偏向"数据契约"(更像 Java 的 record 或 DTO 类)。
和 Java 接口的关键差异:
- TS 接口描述数据结构,不像 Java 接口那样只能定义方法
- TS 接口可以重复声明,同名会自动合并(declaration merging)
- 实现接口不用
implements关键字,结构匹配就算实现(结构型类型系统,下文讲)
interface vs type 别名
两者在大多数场景可以互换:
interface User { name: string }
type User2 = { name: string }
选择规则:
- 对象结构优先用
interface(更符合语义,支持声明合并) - 联合类型、元组、映射类型用
type(interface做不了)
5. 结构型类型系统(Structural Typing)
这是 TS 和 Java 最本质的差异。
interface Duck {
quack(): void
}
class Dog {
quack() { // Dog 没有 implements Duck,但有 quack 方法
console.log('...wang?')
}
}
const d: Duck = new Dog() // ✅ 合法!结构匹配就算 Duck
Java 对照:
- Java 是名义型类型系统(Nominal Typing):必须显式
implements Duck才算 Duck - TS 是结构型类型系统(Structural Typing):长得像就是,不需要声明继承关系
这像什么?像 Go 的 interface。写前端时遇到"诶这个对象怎么我一字不写就能当参数传"别慌,这是 TS 的特性,不是 bug。
6. 泛型:和 Java 几乎一样
// 泛型函数
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
first<number>([1, 2, 3]) // 显式指定 T = number
first([1, 2, 3]) // 不写也行,TS 能推导出 T = number
// 泛型接口
interface ApiResult<T> {
code: number
data: T
}
const res: ApiResult<User> = { code: 0, data: { id: 1, name: 'foo', createdAt: new Date() } }
// 泛型约束(类似 Java 的 <T extends Comparable>)
function getLength<T extends { length: number }>(x: T) {
return x.length
}
getLength('hello') // ✅ string 有 length
getLength([1, 2, 3]) // ✅ 数组有 length
getLength(123) // ❌ number 没有 length
Java 对照:
<T>语法一致T extends Something语法一致- 差别:TS 的泛型是编译时擦除的(和 Java 一样),但类型推导更强,很多时候不用显式指定
7. 字面量类型和枚举
// 字面量类型:值只能是具体的某几个字符串/数字
type Size = 'small' | 'medium' | 'large'
let size: Size = 'small'
size = 'xlarge' // ❌ 报错
// 枚举
enum Role {
Admin = 'ADMIN',
User = 'USER',
Guest = 'GUEST',
}
const r: Role = Role.Admin
Java 对照:
- 字面量联合类型在 Java 里没直接对应物(Java 14+ 的 sealed + String 值可以模拟)
- TS 的
enum和 Java 的enum语法相似,但前端社区更推荐用字面量联合类型type Role = 'admin' | 'user' | 'guest',产物更小、更灵活
8. 函数类型
// 函数类型声明
type BinaryOp = (a: number, b: number) => number
const add: BinaryOp = (a, b) => a + b
const sub: BinaryOp = (a, b) => a - b
// 可选参数和默认参数
function greet(name: string, greeting: string = 'Hello') {
return `${greeting}, ${name}!`
}
Java 对照:函数类型相当于 Java 的 BiFunction<Integer, Integer, Integer>,但 TS 写起来短得多。
9. 工具类型:TS 的"装备栏"
TS 内置了一堆操作类型的工具类型,Vue 和 Element Plus 的类型里到处都是它们:
interface User {
id: number
name: string
email: string
}
// Partial:所有属性变可选(常用于 PATCH 接口)
type UserPatch = Partial<User>
// 等价于 { id?: number; name?: string; email?: string }
// Required:所有属性变必填
type UserStrict = Required<User>
// Readonly:所有属性变只读
type UserReadonly = Readonly<User>
// Pick:只挑选部分字段
type UserSummary = Pick<User, 'id' | 'name'>
// Omit:排除部分字段
type UserCreate = Omit<User, 'id'> // 创建时还没有 id
// Record:构造 Map 类型
type UserMap = Record<number, User> // 等价于 { [key: number]: User }
Java 对照:Java 反射能做类似事但笨重得多。TS 的工具类型是编译时的字符级转换,运行时零开销。
常用组合(项目里的 src/types/note.ts 已经用了):
// 从完整 Note 中去掉 raw 和 html,用于列表场景
export type NoteListItem = Omit<Note, 'raw' | 'html'>
10. 类型断言 vs 类型守卫
有时 TS 推不准,需要手动告诉它"相信我,这玩意就是 XXX":
// 类型断言(等价于 Java 的强制类型转换 (T) obj)
const el = document.querySelector('#app') as HTMLDivElement
el.style.color = 'red'
// 类型守卫:通过函数帮 TS 推断
function isUser(x: unknown): x is User {
return typeof x === 'object' && x !== null && 'name' in x
}
function handle(input: unknown) {
if (isUser(input)) {
input.name // ✅ TS 知道这里 input 是 User
}
}
原则:尽量少用类型断言 as,多用类型守卫。断言是告诉 TS "闭嘴我对",守卫是教 TS "这样推导"。
特殊的 as const:
const config = {
mode: 'dev',
port: 3000,
} as const
// 推导类型:{ readonly mode: 'dev'; readonly port: 3000 }
把对象冻成字面量类型,写配置常量时很好用。
11. 实战:看懂项目里的类型声明
回头看看本项目 src/types/note.ts:
export interface NoteMeta {
title: string
category: string
order: number
date: string
tags: string[]
summary: string
}
export interface Note extends NoteMeta {
slug: string
raw: string
html: string
}
export type NoteListItem = Omit<Note, 'raw' | 'html'>
读懂的关键:
NoteMeta是笔记头部 Frontmatter 的形状(纯数据)Note extends NoteMeta在 interface 里就是继承,Note包含NoteMeta的所有字段再加三个自己的NoteListItem = Omit<Note, 'raw' | 'html'>从 Note 里去掉两个大字段,列表场景用,减小数据量
这三行类型,换成 Java 需要三个 class + 若干 DTO 转换方法。TS 靠类型组合零运行时成本表达了同样的语义。
小结:速查表
| 特性 | 一句话 |
|---|---|
string / number / boolean |
基础类型,全小写 |
let x: T[] = [] |
数组类型 |
string | number |
联合类型,JS 特有 |
interface |
定义对象/结构的契约 |
type |
类型别名,能写联合/元组/映射 |
| 结构型类型 | 长得像就是,不用显式 implements |
泛型 <T> |
和 Java 一样,但推导更强 |
字面量类型 'a' | 'b' |
替代大多数 enum |
Partial / Pick / Omit |
工具类型,操作已有类型 |
as / 类型守卫 |
告诉 TS 真实类型,少用断言多用守卫 |
延伸阅读
- TypeScript 官方手册(中文) — 权威,建议从 Everyday Types 章节开始
- TypeScript Deep Dive — 免费好书,讲到原理层
- Type Challenges — 类型体操练习题,想玩花的再看
- Vue 3 + TypeScript 指南 — 下一步直接切到这里
下一篇:包管理与前端工程化(pnpm、package.json、node_modules 机制)