01 - TypeScript 基础
这份笔记的目标
先建立基础, 后是一些前端项目样例分析。
- 知道和JS的差异。
- 看懂常见TS报错。
- 会写基础类型标注。
- 如何在业务场景里用类型减少线上风险。
1. TypeScript 是什么
TypeScript 是 JavaScript 的超集。
TS做了两件事。
- 保留 JS 运行时行为。
- 在开发阶段增加静态类型检查。
TS 会被编译成 JS。 类型信息不会进入运行时。
1.1 为什么团队要上 TS
在中大型前端项目里。 常见问题不是语法问题。 而是改动后影响面不清楚。
TS 解决的核心。
- 改代码前先预警。
- 重构时有类型护栏。
- IDE补全更准,JS的话跳出几个文件IDE就难以在编译阶段报错了。
- 协作时接口契约更清晰。
1.2 和 JS 的直观差异
let price = 99
price = '99元'JS 合法。 TS 默认不允许。
let price: number = 99
price = '99元'
// Type 'string' is not assignable to type 'number'这个报错不是限制开发。 而是在团队开发提交前拦住潜在线上问题。
2. 本地运行
2.1 安装与编译
npm i -D typescript
# 编译
npx tsc --init
# 直接编译+运行,不生成js中间产物
npx tsx .\Hello.ts或者:
npx tsc --noEmit只做类型检查。 不输出编译产物。
又或者用ts-node工具,更方便些:
ts-node Hello.ts2.2 一个最小可用 ts config
配置可能如下所示:
{
"compilerOptions": {
"target": "ES2020", // 编译产物的目标语法版本
"module": "ESNext", // 产物使用的模块系统
"moduleResolution": "Bundler", // 模块解析策略适配打包工具
"strict": true, // 启用所有严格类型检查开关
"noUncheckedIndexedAccess": true, // 索引访问返回类型自动补充 undefined
"exactOptionalPropertyTypes": true, // 严格区分可选属性与显式 undefined 赋值
"skipLibCheck": true, // 跳过 .d.ts 声明文件检查以提升编译速度
"isolatedModules": true, // 确保每个文件可以独立安全编译
"esModuleInterop": true, // 兼容处理 CommonJS 与 ES 模块差异
"forceConsistentCasingInFileNames": true // 强制校验文件名大小写一致性
},
"include": ["src"] // 指定需要编译的文件或目录范围
}3. 基础类型与类型推断
3.1 常见基础类型
stringnumberbooleannullundefinedbigintsymbolobject
示例:
const username: string = 'kuro'
const age: number = 12
const isAdmin: boolean = false
const id: bigint = 100n3.2 推断优先 标注兜底
TS 能推断就让它推断,复杂再标注。
const count = 1
// 推断为 number
const list = [1, 2, 3]
// 推断为 number[]但函数入参和公共返回值。 建议显式写类型。
function format(N: number): string {
return `${(N / 100).toFixed(2)}`
}3.3 大写类型尽量不用
string > String。 number > Number。
小写 string number boolean 表示原始类型 primitive。 大写 String Number Boolean 表示包装对象类型 object wrapper。
所以大写常关联包装对象 wrapper object。 多数业务并不需要,并且对象相互比较也麻烦。
4. 特殊类型 any unknown never void
4.1 any
长期滥用会失去 TS 意义。
let data: any = await fetch('/api').then(r => r.json())
data.notExist.call()
// 不报类型错 但运行时可能炸,可以临时查看fetch结果(但其实用各种后端工具看接口更直观就是了)4.2 unknown
更安全。 先收窄再使用。
function safeParse(json: string): unknown {
return JSON.parse(json)
}
const value = safeParse('{"name":"A"}')
if (typeof value === 'object' && value !== null && 'name' in value) {
// 在这个分支内 类型被收窄,如果不收窄,无法正常使用
value.age='19'; // 报错,因为没有判断age('age' in value)
value.name='B'; // IDE不报错
}4.3 void
表示函数没有返回值。
function logMessage(msg: string): void {
console.log(msg)
}4.4 never
表示不可能到达。 常用于穷尽检查。
type Role = 'admin' | 'user'
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`)
}
function getHome(role: Role): string {
switch (role) {
case 'admin':
return '/admin'
case 'user':
return '/home'
default:
return assertNever(role)
}
}5. 函数类型
5.1 参数和返回值
function sum(a: number, b: number): number {
return a + b
}5.2 函数类型别名
type RequestFn = (url: string, timeout: number) => Promise<Response>
const request: RequestFn = (url, timeout) => {
return fetch(url, { signal: AbortSignal.timeout(timeout) })
}5.3 可选参数与默认值
function greet(name: string, title?: string): string {
return title ? `${title} ${name}` : name
}
function greet2(name: string, title: string = '同学'): string {
return `${title} ${name}`
}5.4 场景分析 表单提交回调
JS 常见写法。
function submitForm(data, onSuccess) {
// ...
onSuccess('ok')
}这里的短板在于。 onSuccess 参数类型不明确。 调用方只能靠约定记忆写回调结构。
TS 改造。
type SubmitPayload = {
mobile: string
code: string
}
type SubmitResult = {
token: string
expireAt: number
}
function submitForm(
data: SubmitPayload,
onSuccess: (res: SubmitResult) => void
): Promise<void> {
return Promise.resolve().then(() => {
// 回调参数结构在这里被约束,或者说,传参传什么更加清晰了
onSuccess({ token: 't-1', expireAt: Date.now() + 3600_000 })
})
}这种写法带来的直接变化。
- 调用方立刻拿到返回结构补全。
- 回调参数写错字段直接报错。
6. 对象类型 interface 与 type
6.1 interface
适合描述对象结构。
interface UserProfile {
id: string
nickname: string
avatar?: string
}6.2 type
更灵活。 能做 联合 交叉 映射。
type ID = string | number
type UserLite = {
id: ID
nickname: string
}6.3 选型建议
- 业务实体对象优先
interface - 组合类型优先
type - 团队统一比个人偏好更重要
6.4 只读与可选
interface Product {
readonly sku: string
title: string
desc?: string
}readonly 防止误改。 ? 表示属性可选。
7. 联合类型 交叉类型 字面量类型
7.1 联合类型
type Status = 'idle' | 'loading' | 'success' | 'error'比 string 更精确。
7.2 交叉类型
type WithTime = { createdAt: number }
type WithOperator = { operator: string }
type AuditInfo = WithTime & WithOperator7.3 场景分析 接口状态管理
JS 常见写一个状态对象:
const state = {
status: 'ok'
}这类状态对象在后续维护里容易失控。 status 很容易被写入任意字符串。
TS 做法。
type LoadState = 'idle' | 'loading' | 'success' | 'error'
interface PageState {
status: LoadState
data: string[]
errorMsg?: string
}
const state: PageState = {
// 只能是联合类型中的四种状态
status: 'loading',
data: []
}把状态空间锁定。 分支逻辑更可控。
8. 类型收窄 narrowing
联合类型很好用。 但使用前要收窄。
8.1 typeof
/**
* 获取字符串或字符串数组的长度
* @param v {string | string[]} 传入的参数可能是一个字符串,也可能是一个字符串数组
* @returns {number} 返回长度
*/
function getLen(v: string | string[]): number {
// 使用 `typeof` 操作符是 TS 中常见的收窄基本类型的方法。
// 当 TS 引擎遇到 `typeof v === 'string'` 时,会启动类型收窄机制。
if (typeof v === 'string') {
// 此时在这个 if 块内部,v 的类型已经被剥离了 `string[]` 的可能性。
// TS 明确知道 v 现在的类型是精确的 `string`。
// 将鼠标悬浮在 v 上,IDE 也会提示此时 v: string。
return v.length
}
// 因为上面的 if 块中包含了一个 `return` 语句
// TS 编译器会沿着代码的执行路径向下分析。
// 既然执行到了这一行,说明 `v` 绝对不可能是 'string'。
// 因此,TS 自动将联合类型 `string | string[]` 中剔除了 `string`,
// 此时 v 的类型被自然地收窄成了剩下的 `string[]`。
return v.length
}8.2 in
type Cat = { meow: () => void }
type Dog = { bark: () => void }
function speak(pet: Cat | Dog): void {
if ('meow' in pet) {
pet.meow()
} else {
pet.bark()
}
}8.3 instanceof
function formatDate(v: Date | string): string {
if (v instanceof Date) return v.toISOString()
return new Date(v).toISOString()
}8.4 场景分析 后端字段不稳定
实际接口常见,某字段有时是数字,有时是字符串。
type ApiOrder = {
orderId: string | number
}
function normalizeOrderId(order: ApiOrder): string {
if (typeof order.orderId === 'number') {
return String(order.orderId)
}
return order.orderId
}这个阶段不要图快用 as string。 先判断。 再收窄。
9. 泛型 generics
泛型的本质。 把类型当参数。
9.1 基础泛型函数
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
const n = first([1, 2, 3])
const s = first(['a', 'b'])输入类型和返回类型自动联动。 减少重复定义,也避免用 any。
9.2 多类型参数
function pair<K, V>(key: K, value: V): { key: K; value: V } {
return { key, value }
}
const p1 = pair('age', 12)
// p1 推断为 { key: string; value: number }
const p2 = pair<string, boolean>('isAdmin', true)
// p2 显式指定类型为 { key: string; value: boolean }多个字段的类型关系可以同时表达。 对象结构更清晰,调用点补全也更准确。
9.3 泛型约束
function getLength<T extends { length: number }>(v: T): number {
return v.length
}既保留泛型灵活性,又限制了最小能力边界。 调用时可提前拦截不满足约束的参数。
9.4 场景分析 通用请求封装
JS 版本。
async function get(url) {
const res = await fetch(url)
return res.json()
}在这种写法里。 调用方只能拿到一个宽泛结果。 字段约束基本依赖人为记忆。
TS 泛型版。
type ApiResp<T> = {
code: number
message: string
data: T
}
async function getJSON<T>(url: string): Promise<ApiResp<T>> {
const res = await fetch(url)
return res.json() as Promise<ApiResp<T>>
}
type Todo = { id: number; title: string; done: boolean }
async function demo(): Promise<void> {
// 这里显式传入 Todo[],告诉函数 data 的实际形状
// 并且针对不同接口,也可以传不同的类型
const resp = await getJSON<Todo[]>('/api/todos')
// resp 自动推断为 ApiResp<Todo[]>
// 因此 resp.data 自动推断为 Todo[]
resp.data[0].title
// 如果写成 resp.data[0].titttile 会直接报错
// 如果把 done 当字符串处理也会报错,例如 resp.data[0].done.trim()
}这段泛型写法带来的直接效果。
- 调用点拿到完整类型。
- 字段拼写错误IDE会抛错。
10. 数组 元组 枚举
10.1 数组
const ids: number[] = [1, 2, 3]
const names: Array<string> = ['a', 'b']10.2 元组 tuple
适合固定长度 固定语义位置。
type Point = [number, number]
const p: Point = [120.1, 30.2]10.3 枚举 enum
enum Permission {
Read = 'READ',
Write = 'WRITE'
}如果追求更轻量。 优先字面量联合。
11. 类 class 在 TS 里的常见用法
11.1 访问修饰符
publicprivateprotected
class Counter {
private value = 0
inc(): void {
this.value += 1
}
getValue(): number {
return this.value
}
}11.2 implements 接口
interface StorageLike {
get(key: string): string | null
set(key: string, value: string): void
}
class LocalStorageAdapter implements StorageLike {
get(key: string): string | null {
return localStorage.getItem(key)
}
set(key: string, value: string): void {
localStorage.setItem(key, value)
}
}12. 断言 as 与非空断言
12.1 类型断言 as
在比编译器更确定时使用。
const el = document.getElementById('app') as HTMLDivElement
el.innerText = 'hello'12.2 非空断言
const root = document.getElementById('root')!
root.innerHTML = 'ok'但如果真实是 null。 运行时仍会报错。
能判断就判断。 少用感叹号。
13. tsconfig 常用项
13.1 严格性相关
strict: 开启所有严格模式检查选项noImplicitAny: 禁止隐式推断为any类型strictNullChecks: 严格检查null与undefined的赋值noUncheckedIndexedAccess: 索引访问返回联合类型追加undefined
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true
}
}13.2 产物相关
target: 设置编译出的 JS 语法版本module: 指定生成代码的模块化规范outDir: 编译产物的存放目录sourceMap: 是否生成源代码映射文件
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"outDir": "./dist",
"sourceMap": true
}
}13.3 工程化相关
baseUrl: 设定解析非绝对模块名称的基本目录paths: 设定模块名到基于baseUrl的路径映射allowJs: 允许编译器编译 JS 文件checkJs: 报告 JS 文件中的错误
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"allowJs": true,
"checkJs": false
}
}迁移老项目时。 可以先开 allowJs。 逐步把 JS 改成 TS。
14. 与 JS 对比的三个高频场景
场景一 组件 props 约束
JS代码:
function UserCard(props) {
return `<h3>${props.name}</h3>`
}这种写法没有明确参数契约。 name 拼错时通常只能运行后暴露。
TS代码:
type UserCardProps = {
name: string
age?: number
}
function UserCard(props: UserCardProps): string {
// props.name 有稳定的类型提示与检查
return `<h3>${props.name}</h3>`
}场景二 字段改名的连锁风险
接口通常会出现字段变更。 例如从 nickName 改成 nickname。
JS 场景下多数调用点运行前很难发现异常。
function printUser(user) {
// 运行代码之前毫无预警
// 执行时可能静默输出 undefined 或导致后续渲染逻辑崩溃
console.log(user.nickName)
}TS 场景下修改类型定义可拦截风险。 所有遗留的旧字段访问点会在编译阶段统一抛出错误。
type UserInfo = {
id: number
nickname: string // 这里统一更改为最新接口字段
}
function printUser(user: UserInfo): void {
console.log(user.nickName)
// 报错: Property 'nickName' does not exist on type 'UserInfo'. Did you mean 'nickname'?
}场景三 列表渲染 key 类型
原先正常的类型定义,id 为字符串。
如果使用 TS,改动类型后,涉及到的属性方法处理会全盘标红。
type Item = { id: number; title: string } // 这里从 string 改成 number
function render(items: Item[]): string {
// 假设旧代码原本需要用 substring 截取前两位
return items.map(item => `<li data-key="${item.id.substring(0, 2)}">${item.title}</li>`).join('')
// 此时这里会在编译时抛错: Property 'substring' does not exist on type 'number'.
}