Skip to content

01 - TypeScript 基础

这份笔记的目标

先建立基础, 后是一些前端项目样例分析。

  • 知道和JS的差异。
  • 看懂常见TS报错。
  • 会写基础类型标注。
  • 如何在业务场景里用类型减少线上风险。

1. TypeScript 是什么

TypeScript 是 JavaScript 的超集。

TS做了两件事。

  • 保留 JS 运行时行为。
  • 在开发阶段增加静态类型检查。

TS 会被编译成 JS。 类型信息不会进入运行时。

1.1 为什么团队要上 TS

在中大型前端项目里。 常见问题不是语法问题。 而是改动后影响面不清楚

TS 解决的核心。

  • 改代码前先预警。
  • 重构时有类型护栏。
  • IDE补全更准,JS的话跳出几个文件IDE就难以在编译阶段报错了。
  • 协作时接口契约更清晰。

1.2 和 JS 的直观差异

js
let price = 99
price = '99元'

JS 合法。 TS 默认不允许。

ts
let price: number = 99
price = '99元'
// Type 'string' is not assignable to type 'number'

这个报错不是限制开发。 而是在团队开发提交前拦住潜在线上问题。


2. 本地运行

2.1 安装与编译

bash
npm i -D typescript
# 编译
npx tsc --init
# 直接编译+运行,不生成js中间产物
npx tsx .\Hello.ts

或者:

bash
npx tsc --noEmit

只做类型检查。 不输出编译产物。

又或者用ts-node工具,更方便些:

bash
ts-node Hello.ts

2.2 一个最小可用 ts config

配置可能如下所示:

json
{
	"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 常见基础类型

  • string
  • number
  • boolean
  • null
  • undefined
  • bigint
  • symbol
  • object

示例:

ts
const username: string = 'kuro'
const age: number = 12
const isAdmin: boolean = false
const id: bigint = 100n

3.2 推断优先 标注兜底

TS 能推断就让它推断,复杂再标注。

ts
const count = 1
// 推断为 number

const list = [1, 2, 3]
// 推断为 number[]

但函数入参和公共返回值。 建议显式写类型。

ts
function format(N: number): string {
	return `${(N / 100).toFixed(2)}`
}

3.3 大写类型尽量不用

string > Stringnumber > Number

小写 string number boolean 表示原始类型 primitive。 大写 String Number Boolean 表示包装对象类型 object wrapper。

所以大写常关联包装对象 wrapper object。 多数业务并不需要,并且对象相互比较也麻烦。


4. 特殊类型 any unknown never void

4.1 any

长期滥用会失去 TS 意义。

ts
let data: any = await fetch('/api').then(r => r.json())
data.notExist.call()
// 不报类型错 但运行时可能炸,可以临时查看fetch结果(但其实用各种后端工具看接口更直观就是了)

4.2 unknown

更安全。 先收窄再使用。

ts
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

表示函数没有返回值。

ts
function logMessage(msg: string): void {
	console.log(msg)
}

4.4 never

表示不可能到达。 常用于穷尽检查。

ts
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 参数和返回值

ts
function sum(a: number, b: number): number {
	return a + b
}

5.2 函数类型别名

ts
type RequestFn = (url: string, timeout: number) => Promise<Response>

const request: RequestFn = (url, timeout) => {
	return fetch(url, { signal: AbortSignal.timeout(timeout) })
}

5.3 可选参数与默认值

ts
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 常见写法。

js
function submitForm(data, onSuccess) {
	// ...
	onSuccess('ok')
}

这里的短板在于。 onSuccess 参数类型不明确。 调用方只能靠约定记忆写回调结构。

TS 改造。

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

适合描述对象结构。

ts
interface UserProfile {
	id: string
	nickname: string
	avatar?: string
}

6.2 type

更灵活。 能做 联合 交叉 映射。

ts
type ID = string | number

type UserLite = {
	id: ID
	nickname: string
}

6.3 选型建议

  • 业务实体对象优先 interface
  • 组合类型优先 type
  • 团队统一比个人偏好更重要

6.4 只读与可选

ts
interface Product {
	readonly sku: string
	title: string
	desc?: string
}

readonly 防止误改。 ? 表示属性可选。


7. 联合类型 交叉类型 字面量类型

7.1 联合类型

ts
type Status = 'idle' | 'loading' | 'success' | 'error'

string 更精确。

7.2 交叉类型

ts
type WithTime = { createdAt: number }
type WithOperator = { operator: string }

type AuditInfo = WithTime & WithOperator

7.3 场景分析 接口状态管理

JS 常见写一个状态对象:

js
const state = {
	status: 'ok'
}

这类状态对象在后续维护里容易失控。 status 很容易被写入任意字符串。

TS 做法。

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

ts
/**
 * 获取字符串或字符串数组的长度
 * @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

ts
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

ts
function formatDate(v: Date | string): string {
	if (v instanceof Date) return v.toISOString()
	return new Date(v).toISOString()
}

8.4 场景分析 后端字段不稳定

实际接口常见,某字段有时是数字,有时是字符串。

ts
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 基础泛型函数

ts
function first<T>(arr: T[]): T | undefined {
	return arr[0]
}

const n = first([1, 2, 3])
const s = first(['a', 'b'])

输入类型和返回类型自动联动。 减少重复定义,也避免用 any。

9.2 多类型参数

ts
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 泛型约束

ts
function getLength<T extends { length: number }>(v: T): number {
	return v.length
}

既保留泛型灵活性,又限制了最小能力边界。 调用时可提前拦截不满足约束的参数。

9.4 场景分析 通用请求封装

JS 版本。

js
async function get(url) {
	const res = await fetch(url)
	return res.json()
}

在这种写法里。 调用方只能拿到一个宽泛结果。 字段约束基本依赖人为记忆。

TS 泛型版。

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 数组

ts
const ids: number[] = [1, 2, 3]
const names: Array<string> = ['a', 'b']

10.2 元组 tuple

适合固定长度 固定语义位置。

ts
type Point = [number, number]
const p: Point = [120.1, 30.2]

10.3 枚举 enum

ts
enum Permission {
	Read = 'READ',
	Write = 'WRITE'
}

如果追求更轻量。 优先字面量联合。


11. 类 class 在 TS 里的常见用法

11.1 访问修饰符

  • public
  • private
  • protected
ts
class Counter {
	private value = 0

	inc(): void {
		this.value += 1
	}

	getValue(): number {
		return this.value
	}
}

11.2 implements 接口

ts
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

在比编译器更确定时使用。

ts
const el = document.getElementById('app') as HTMLDivElement
el.innerText = 'hello'

12.2 非空断言

ts
const root = document.getElementById('root')!
root.innerHTML = 'ok'

但如果真实是 null。 运行时仍会报错。

能判断就判断。 少用感叹号。


13. tsconfig 常用项

13.1 严格性相关

  • strict: 开启所有严格模式检查选项
  • noImplicitAny: 禁止隐式推断为 any 类型
  • strictNullChecks: 严格检查 nullundefined 的赋值
  • noUncheckedIndexedAccess: 索引访问返回联合类型追加 undefined
json
{
	"compilerOptions": {
		"strict": true,
		"noImplicitAny": true,
		"strictNullChecks": true,
		"noUncheckedIndexedAccess": true
	}
}

13.2 产物相关

  • target: 设置编译出的 JS 语法版本
  • module: 指定生成代码的模块化规范
  • outDir: 编译产物的存放目录
  • sourceMap: 是否生成源代码映射文件
json
{
	"compilerOptions": {
		"target": "ES6",
		"module": "CommonJS",
		"outDir": "./dist",
		"sourceMap": true
	}
}

13.3 工程化相关

  • baseUrl: 设定解析非绝对模块名称的基本目录
  • paths: 设定模块名到基于 baseUrl 的路径映射
  • allowJs: 允许编译器编译 JS 文件
  • checkJs: 报告 JS 文件中的错误
json
{
	"compilerOptions": {
		"baseUrl": ".",
		"paths": {
			"@/*": ["src/*"]
		},
		"allowJs": true,
		"checkJs": false
	}
}

迁移老项目时。 可以先开 allowJs。 逐步把 JS 改成 TS。


14. 与 JS 对比的三个高频场景

场景一 组件 props 约束

JS代码:

js
function UserCard(props) {
	return `<h3>${props.name}</h3>`
}

这种写法没有明确参数契约。 name 拼错时通常只能运行后暴露。

TS代码:

ts
type UserCardProps = {
	name: string
	age?: number
}

function UserCard(props: UserCardProps): string {
	// props.name 有稳定的类型提示与检查
	return `<h3>${props.name}</h3>`
}

场景二 字段改名的连锁风险

接口通常会出现字段变更。 例如从 nickName 改成 nickname

JS 场景下多数调用点运行前很难发现异常。

javascript
function printUser(user) {
	// 运行代码之前毫无预警
	// 执行时可能静默输出 undefined 或导致后续渲染逻辑崩溃
	console.log(user.nickName)
}

TS 场景下修改类型定义可拦截风险。 所有遗留的旧字段访问点会在编译阶段统一抛出错误。

typescript
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,改动类型后,涉及到的属性方法处理会全盘标红。

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

<< 返回首页