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]

注意


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 接口的关键差异

interface vs type 别名

两者在大多数场景可以互换:

interface User { name: string }
type User2 = { name: string }

选择规则:


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 对照

这像什么?像 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 对照


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 对照


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'>

读懂的关键:

  1. NoteMeta 是笔记头部 Frontmatter 的形状(纯数据)
  2. Note extends NoteMetainterface 里就是继承,Note 包含 NoteMeta 的所有字段再加三个自己的
  3. 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 真实类型,少用断言多用守卫

延伸阅读


下一篇:包管理与前端工程化(pnpm、package.json、node_modules 机制)