第7章 7.1 TypeScript 改造 uniapp:从「能跑就行」到「健壮可维护」
🎯 开场:为什么你的 uniapp 代码越改越乱?
上一章我们做了一个完整的跨端电商 App,做完之后满心欢喜准备上线。
但没过几天,你就发现问题了:
- 某个页面报错了,看报错信息根本不知道哪个变量出了问题
- 想复用之前的代码,结果发现变量名乱飞,改一下全崩
- 接手别人的项目,光是搞清楚
data里的字段是啥就要半小时
痛点来了:JavaScript 是门「弱类型」语言,它太灵活了——变量随便改、函数随便传、拼写错了也不报错。结果就是代码一多,bug 就多,维护成本蹭蹭往上涨。
这感觉就像——你有一抽屉杂物,当初扔进去很爽,现在要找东西就头大了。
TypeScript 就是来帮你「断舍离」的。它给 JavaScript 加了一套「收纳规则」,让你的代码整整齐齐、出错秒懂。
这一章,我们把上一章的电商 App 改造成 TypeScript 版本。学完你能:
- 写出有「类型保护」的 uniapp 代码
- 读懂并配置\n\n
\n\n
\n\n tsconfig.json - 用
defineComponent+ 泛型封装出「万年不崩」的请求函数
🧱 基础:TypeScript 核心概念(5 分钟入门)
7.1.1 什么是 TypeScript?—— 给 JavaScript 套个「塑封膜」
是什么:TypeScript = JavaScript + 类型系统。你写的是 .ts 文件,但最后编译完还是变成 .js 给小程序/uniapp 运行。
为什么要用:想象你去快递站寄手机,JavaScript 只要求「包装好」,但 TypeScript 要求「用指定气泡袋 + 纸箱 + 填充物 + 贴易碎标签」。多了一道工序,但东西寄出去更安全。
怎么用——先来看最基础的区别:
// JavaScript:变量可以是任何类型
let username = "小明"
username = 123 // 不报错,但埋雷
// TypeScript:声明时指定类型
let username: string = "小明"
username = 123 // ❌ 报错:不能把 number 赋给 string
这段代码不能直接跑,但你在 VS Code 里写的时候,红色波浪线直接告诉你「类型错了」,而不是等到运行时才崩。
7.1.2 类型注解 —— 给变量贴标签
生活类比:就像超市商品贴价签。「苹果」后面写「¥15/斤」,你一眼就知道价格,不会把苹果当橘子用。
// 基础类型:string / number / boolean
let price: number = 99.9
let name: string = "蓝牙耳机"
let inStock: boolean = true
// 数组:用 [] 表示
let productList: string[] = ["耳机", "手机壳", "充电宝"]
let priceList: number[] = [99, 199, 299]
// 对象:用 interface 描述形状
interface Product {
id: number
name: string
price: number
stock: number
}
let product: Product = {
id: 1,
name: "蓝牙耳机",
price: 299,
stock: 50
}
7.1.3 函数参数和返回值类型 —— 函数的「使用说明书」
生活类比:就像外卖包装上的说明——「加热 2 分钟」「请勿空腹食用」。函数有了类型注解,你调用时就知道该传什么、会返回什么。
// 计算商品折后价
function calcDiscount(price: number, discount: number): number {
if (discount < 0 || discount > 1) {
throw new Error("折扣必须是 0-1 之间的小数")
}
return price * (1 - discount)
}
let finalPrice = calcDiscount(299, 0.2) // ✅ 传入 number,返回 number
let result = calcDiscount("299", 0.2) // ❌ 报错:第一个参数应该是 number
7.1.4 interface 和 type —— 自定义类型的两把刀
是什么:都是用来定义复杂类型的「模具」。
// interface:适合写「对象」的结构
interface User {
id: number
username: string
avatar?: string // 问号表示可选字段
}
// type:更灵活,可以组合、交叉
type OrderStatus = "pending" | "paid" | "shipped" | "completed"
type Order = {
orderId: string
status: OrderStatus
totalPrice: number
}
// 组合类型:VIP用户 = 用户 + 会员等级
type VipUser = User & {
vipLevel: number
expireDate: string
}
7.1.5 uniapp 的 defineComponent —— 写页面的「标准姿势」
生活类比:就像填表格——defineComponent 是个标准表格模板,你照着填「数据放哪」「方法叫什么」「生命周期怎么写」,编译器一眼就懂你要做什么。
import { defineComponent, ref } from 'vue'
export default defineComponent({
// 相当于原来的 data()
data() {
return {
username: "小明",
age: 25
}
},
// 相当于原来的 onLoad / onShow
onLoad() {
console.log("页面加载了")
},
// 相当于原来的 methods
methods: {
sayHi() {
console.log("你好,我叫" + this.username)
}
}
})
但这里有个问题——data() 里的类型全靠「any」,你想加类型保护怎么办?往下看。
🔥 实战:3 个递进项目
📦 项目 1:给商品列表加上类型(10 分钟)
目标:把上一章的商品列表加上完整的 TypeScript 类型定义。
完整代码:
// types/product.ts
// 定义商品类型(单独文件存放,方便复用)
export interface Product {
id: number
name: string
price: number
image: string
stock: number
category: string
}
export interface Category {
id: number
name: string
icon: string
}
// src/pages/index/index.vue
<script setup lang="ts">
import { ref } from 'vue'
import type { Product, Category } from '@/types/product'
// 商品列表 - 现在有完整类型提示
const productList = ref<Product[]>([
{
id: 1,
name: 'AirPods Pro 2',
price: 1799,
image: '/static/headphones.jpg',
stock: 100,
category: '数码'
},
{
id: 2,
name: 'iPhone 15 手机壳',
price: 59,
image: '/static/case.jpg',
stock: 500,
category: '配件'
}
])
// 分类列表
const categoryList = ref<Category[]>([
{ id: 1, name: '数码', icon: '📱' },
{ id: 2, name: '配件', icon: '🎧' },
{ id: 3, name: '周边', icon: '🎁' }
])
// 计算有库存的商品数量
function getAvailableCount(): number {
return productList.value.filter(p => p.stock > 0).length
}
// 添加商品(带完整类型检查)
function addProduct(product: Product): void {
productList.value.push(product)
}
// 预期输出
console.log('商品总数:', productList.value.length) // 商品总数: 2
console.log('有库存商品:', getAvailableCount()) // 有库存商品: 2
</script>
运行结果:
商品总数: 2
有库存商品: 2
一句话解释:用 interface 定义商品结构,ref<Product[]> 让 TypeScript 知道这是「商品数组」,写错字段名立刻报错。
📦 项目 2:泛型请求封装 —— 封装一个「永不错配」的 API 函数(15 分钟)
痛点:每次调接口都要写 uni.request,还要手动处理返回数据的类型,容易漏掉空值判断。
目标:封装一个「传什么类型,返回什么类型」的请求函数。
完整代码:
// src/types/api.ts
// 定义统一的响应结构
export interface ApiResponse<T> {
code: number
message: string
data: T
}
// 定义分页参数
export interface PageParams {
page: number
pageSize: number
}
// src/utils/request.ts
// 泛型函数:T 是返回数据的类型
export function request<T>(options: {
url: string
method?: 'GET' | 'POST'
data?: any
}): Promise<ApiResponse<T>> {
return new Promise((resolve, reject) => {
uni.request({
url: options.url,
method: options.method || 'GET',
data: options.data,
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data as ApiResponse<T>)
} else {
reject(new Error(`请求失败: ${res.statusCode}`))
}
},
fail: (err) => {
reject(err)
}
})
})
}
// src/types/product.ts(继续扩展)
export interface ProductDetail {
id: number
name: string
price: number
stock: number
images: string[]
description: string
}
// src/pages/product/detail.vue
<script setup lang="ts">
import { ref } from 'vue'
import { request } from '@/utils/request'
import type { ApiResponse, ProductDetail } from '@/types'
const product = ref<ProductDetail | null>(null)
async function fetchProductDetail(id: number) {
try {
// 关键:TypeScript 自动知道返回的是 ApiResponse<ProductDetail>
const res = await request<ProductDetail>({
url: '/api/product/detail',
method: 'GET',
data: { id }
})
if (res.code === 0) {
product.value = res.data // ✅ 一定是 ProductDetail 类型
} else {
uni.showToast({ title: res.message })
}
} catch (e) {
uni.showToast({ title: '网络错误' })
}
}
</script>
预期输出(控制台模拟):
请求成功: { code: 0, data: { id: 1, name: 'AirPods', ... } }
一句话解释:泛型 <T> 让请求函数「什么都能返回」,但调用时写清楚类型后,TypeScript 就知道你拿到的到底是什么。
📦 项目 3:组合实战 —— 做个「商品筛选小工具」(15 分钟)
需求:从 JSON 数据中筛选商品,按分类和价格区间过滤,支持排序。
完整代码:
// src/types/product.ts
export interface Product {
id: number
name: string
price: number
category: string
sales: number
}
// src/utils/filterProducts.ts
import type { Product } from '@/types/product'
interface FilterOptions {
category?: string
minPrice?: number
maxPrice?: number
sortBy?: 'price' | 'sales'
sortOrder?: 'asc' | 'desc'
}
export function filterProducts(
products: Product[],
options: FilterOptions
): Product[] {
let result = [...products] // 浅拷贝,不修改原数组
// 按分类过滤
if (options.category) {
result = result.filter(p => p.category === options.category)
}
// 按价格区间过滤
if (options.minPrice !== undefined) {
result = result.filter(p => p.price >= options.minPrice)
}
if (options.maxPrice !== undefined) {
result = result.filter(p => p.price <= options.maxPrice)
}
// 排序
if (options.sortBy) {
result.sort((a, b) => {
const aVal = a[options.sortBy!]
const bVal = b[options.sortBy!]
const order = options.sortOrder === 'asc' ? 1 : -1
return (aVal - bVal) * order
})
}
return result
}
// src/pages/search/index.vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { filterProducts } from '@/utils/filterProducts'
import type { Product } from '@/types/product'
// 模拟商品数据
const allProducts = ref<Product[]>([
{ id: 1, name: 'AirPods Pro', price: 1799, category: '数码', sales: 999 },
{ id: 2, name: '手机壳', price: 59, category: '配件', sales: 500 },
{ id: 3, name: 'iPad 保护套', price: 199, category: '配件', sales: 300 },
{ id: 4, name: 'Apple Watch', price: 2999, category: '数码', sales: 800 },
{ id: 5, name: 'MagSafe 充电器', price: 329, category: '配件', sales: 200 }
])
// 筛选条件
const selectedCategory = ref<string>('')
const minPrice = ref<number>(0)
const maxPrice = ref<number>(10000)
const sortBy = ref<'price' | 'sales'>('sales')
// 筛选结果(响应式计算)
const filteredProducts = computed(() => {
return filterProducts(allProducts.value, {
category: selectedCategory.value || undefined,
minPrice: minPrice.value,
maxPrice: maxPrice.value,
sortBy: sortBy.value,
sortOrder: 'desc'
})
})
// 测试输出
console.log('全部商品:', allProducts.value.length) // 全部商品: 5
console.log('筛选后:', filteredProducts.value.map(p => p.name))
// 筛选后: ['AirPods Pro', 'Apple Watch', '手机壳', 'iPad 保护套', 'MagSafe 充电器']
</script>
预期输出:
全部商品: 5
筛选后: ['AirPods Pro', 'Apple Watch', '手机壳', 'iPad 保护套', 'MagSafe 充电器']
一句话解释:把筛选逻辑抽成纯函数,用 interface FilterOptions 约束参数类型,调用时想拼错都难。
💪 进阶:5 个新手必踩的坑 + 调试技巧
坑 1:any 类型是「万能药」也是「万恶之源」
// ❌ 错误:滥用 any,类型检查完全失效
function getProduct(id: any): any {
return { id, name: '商品', price: 99 }
}
// ✅ 正确:用 unknown 代替 any,强制做类型检查
function getProduct(id: number): unknown {
if (id <= 0) throw new Error('ID 必须大于 0')
return { id, name: '商品', price: 99 }
}
坑 2:可选参数漏写 ?
// ❌ 错误:忘记加问号,TypeScript 认为必须传
function createUser(name: string, age: number) { ... }
createUser('小明') // ❌ 报错:缺少 age
// ✅ 正确:可选参数加 ?
function createUser(name: string, age?: number) { ... }
createUser('小明') // ✅ OK,age 是 undefined
坑 3:数组方法里拿不到正确的 this 类型
// ❌ 错误:forEach 回调里 this 是 undefined
const products = [{ price: 100 }, { price: 200 }]
products.forEach(function(p) {
console.log(this.price) // ❌ this 类型不对
})
// ✅ 正确:用箭头函数或声明 this 类型
products.forEach((p) => {
console.log(p.price) // ✅ 直接访问
})
坑 4:interface 和 type 混用引发困惑
// ❌ 混乱:同一个概念两套定义
interface User { id: number }
type User = { id: number } // 冲突了
// ✅ 统一:对象用 interface,基础类型组合用 type
interface User { id: number; name: string }
type UserStatus = 'active' | 'inactive'
坑 5:泛型约束没写对
// ❌ 错误:T 是泛型,但没约束,访问 T.name 会报错
function printName<T>(item: T): string {
return item.name // ❌ T 可能是任何类型,不一定有 name
}
// ✅ 正确:用 extends 约束泛型
function printName<T extends { name: string }>(item: T): string {
return item.name // ✅ TypeScript 知道 T 一定有 name
}
调试技巧:用 console.log 配合 JSON.stringify
// 打印对象时用这个,避免 [object Object]
console.log('商品信息:', JSON.stringify(product, null, 2))
// 复杂数据用 table 格式(小程序控制台支持)
console.table(productList)
✏️ 练习题
练习 1(2 分钟):给变量加类型
- 输入:有一行 let count = 0
- 预期输出:改成 let count: number = 0
- 提示:数字变量用 number 类型
练习 2(2 分钟):给函数加返回类型
- 输入:有一个函数 function add(a, b) { return a + b }
- 预期输出:加上参数和返回类型 function add(a: number, b: number): number
- 提示:参数和返回值都要加类型
练习 3(3 分钟):用 interface 定义用户
- 输入:定义一个用户对象 { id: 1, name: '小明', email: 'xiaoming@example.com' }
- 预期输出:写出对应的 interface User
- 提示:可选字段(可能有值也可能没有)加 ?
练习 4(5 分钟):用泛型函数封装
- 输入:写一个函数,传入数组,返回第一个元素
- 预期输出:function first<T>(arr: T[]): T | undefined { return arr[0] }
- 提示:泛型 <T> 让函数能处理任意类型的数组
练习 5(5 分钟):找出类型错误
- 输入:以下代码有一处类型错误
interface Config { url: string }
function fetch(config: Config) { ... }
fetch({ path: '/api' }) // 报错
- 预期输出:说明哪里错了并改正
- 提示:看 Config 定义的字段名和调用时传的是否一致
作业:做一个「TypeScript 购物车工具」
- 需求描述:用 TypeScript 实现一个购物车,包含商品列表、添加商品、删除商品、计算总价功能
- 功能点:
1. 用interface定义CartItem类型
2. 用泛型封装一个「从数组中查找商品」的函数
3. 计算总价时用reduce,并且类型要正确 - 加分项:
1. 添加「库存不足」的类型检查
2. 用type定义CartStatus联合类型 - 验收标准:TS 编译无错误、能正常添加/删除/计算
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结
这一章学了 3 件事:
- TypeScript = JavaScript + 类型系统,编译后还是 JS,但开发时更安全
interface和type用来定义复杂类型,让对象结构一目了然泛型让函数更灵活,一个函数能处理多种类型
延伸学习资源:
- TypeScript 官方文档(中文版)
- 掘金小册《TypeScript 全面进阶指南》
- 视频:B 站「TypeScript 入门与实战」
互动钩子:
你在项目中有没有遇到过「类型不匹配」的 bug?是怎么排查的?评论区聊聊老粉优先回复!
下一章剧透:
项目做大了,组件之间的数据传来传去变得越来越乱……下一章我们要学一个「全局数据仓库」——Pinia 状态管理,让跨组件共享数据变得清清楚楚。

评论(0)