第三篇:TypeScript 开发微信小程序的避坑指南与实战技巧
TypeScript 开发微信小程序的避坑指南与实战技巧
本文已收录于 CSDN 专栏《微信小程序云开发实战:宠物上门预约系统全流程》,原创不易,欢迎点赞、收藏、关注。
前言
TypeScript 早已成为前端开发的工业级标准,微信小程序官方也早已原生支持 TypeScript(以下简称 TS)开发。但在实际的小程序开发中,尤其是结合微信原生 API、云开发、自定义组件的场景下,我们会遇到大量的类型丢失、适配错误、this 指向异常等坑,很多开发者最终把 TS 写成了 AnyScript,完全失去了 TS 的类型安全优势。
本文基于宠物上门预约小程序的完整 TS 实战经验,整理了小程序 TS 开发中高频踩坑的解决方案,以及能显著提升开发效率的实战技巧,所有内容均经过线上项目验证,可直接落地使用。
一、微信小程序 TS 开发环境基础搭建与配置
1.1 项目初始化
全新项目
直接在微信开发者工具中,新建项目时选择「不使用云服务」/「微信云开发」,语言选择「TypeScript」,即可生成官方的 TS 模板项目,开箱即用。
现有 JS 项目升级 TS
- 在项目根目录创建
tsconfig.json配置文件 - 安装微信小程序类型定义包:
npm install miniprogram-api-typings -D - 将
.js文件后缀改为.ts,逐步补充类型定义 - 开启微信开发者工具的「增强编译」、「ES6 转 ES5」功能
1.2 核心配置文件 tsconfig.json
很多坑都源于 tsconfig 的配置错误,这里给出经过线上验证的最优配置,解决大部分基础类型问题:
json
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020", "DOM"],
"declaration": false,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"resolveJsonModule": true,
// 类型定义包路径配置,核心!解决微信API类型找不到的问题
"typeRoots": ["./typings", "./node_modules/@types", "./node_modules"],
"types": ["miniprogram-api-typings", "node"],
// 路径别名配置,解决分包引用类型找不到的问题
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@/utils/*": ["./utils/*"],
"@/typings/*": ["./typings/*"]
}
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "typings", "cloudfunctions"]
}
1.3 云函数的 TS 配置
云函数运行在 Node.js 环境,需要单独配置 tsconfig,避免和前端小程序环境冲突,在每个云函数目录下创建tsconfig.json:
json
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"typeRoots": ["./typings", "../node_modules/@types"],
"types": ["node", "wx-server-sdk"]
},
"include": ["index.ts"],
"exclude": ["node_modules"]
}
同时安装云函数所需的类型包:npm install @types/node wx-server-sdk -D
二、高频踩坑指南与解决方案
坑 1:Page/Component 实例的 this 指向与类型完全丢失
问题描述
这是小程序 TS 开发最常见的坑:使用原生Page()/Component()构造器时,TS 无法推断this的类型,导致this.data.xxx、this.methods.xxx全部是any类型,没有任何类型提示,甚至会报类型错误。
错误示例
typescript
运行
Page({
data: {
orderList: [],
page: 1
},
onLoad() {
// 这里this.data.orderList 是any类型,没有类型提示,写错字段也不会报错
console.log(this.data.orderList)
}
})
解决方案
通过扩展微信小程序的命名空间,给 Page/Component 加上泛型约束,实现完整的类型推断,步骤如下:
- 在
typings/index.d.ts中扩展全局类型:
typescript
运行
// 扩展Page的类型定义
declare namespace WechatMiniprogram {
type PageInstance<
TData extends Record<string, any> = {},
TCustom extends Record<string, any> = {}
> = Instance<Page<TData, TCustom>> & TCustom & { data: TData }
// 重写Page构造器的类型
function Page<
TData extends Record<string, any> = {},
TCustom extends Record<string, any> = {}
>(
options: PageOptions<TData, TCustom> & ThisType<PageInstance<TData, TCustom>>
): void
}
- 页面中使用,实现完整的类型推断:
typescript
运行
// 定义页面data的类型
interface PageData {
orderList: OrderItem[];
page: number;
total: number;
loading: boolean;
}
// 定义页面自定义方法的类型
interface PageCustom {
getOrderList: (isRefresh?: boolean) => Promise<void>;
onRefresh: () => void;
}
// 订单列表项类型
interface OrderItem {
order_id: string;
service_name: string;
order_amount: number;
order_status: string;
create_time: number;
}
Page<PageData, PageCustom>({
data: {
orderList: [],
page: 1,
total: 0,
loading: false
},
onLoad() {
// 这里this.data 有完整的类型提示,写错字段会直接报错
this.getOrderList()
},
async getOrderList(isRefresh = false) {
// 方法内部的this也有完整的类型推断
this.setData({ loading: true })
const res = await wx.cloud.callFunction({
name: 'order/getOrderList',
data: { page: this.data.page }
})
// 后续业务逻辑...
},
onRefresh() {
this.setData({ page: 1, orderList: [] })
this.getOrderList(true)
}
})
Component 组件的类型解决方案
同理,给 Component 加上泛型约束,解决 properties、data、methods 的类型丢失问题:
typescript
运行
// typings/index.d.ts 扩展Component类型
declare namespace WechatMiniprogram {
function Component<
TData extends Record<string, any> = {},
TProperties extends Component.PropertyOption = {},
TMethods extends Component.MethodOption = {},
TCustom extends Record<string, any> = {}
>(
options: Component.ComponentOptions<TData, TProperties, TMethods, TCustom> &
ThisType<Component.Instance<TData, TProperties, TMethods, TCustom>>
): void
}
坑 2:小程序 API 的回调与 Promise 类型冲突
问题描述
微信小程序的 API 同时支持回调风格和 Promise 风格,但是官方的类型定义默认是回调风格,使用async/await调用时,会出现返回值类型错误、无类型提示的问题。
错误示例
typescript
运行
// 用async/await调用wx.request,返回值类型是any,没有类型提示
const res = await wx.request({
url: 'https://api.example.com',
method: 'GET'
})
解决方案
封装通用的 Promisify 工具,给所有微信 API 加上完整的 Promise 类型支持,实现代码如下:
typescript
运行
// utils/promisify.ts
type WxApiFn<T extends any[], R> = (...args: T) => void
// 提取微信API的参数类型,排除success/fail/complete回调
type ExtractWxApiParams<T> = T extends WxApiFn<infer P, any>
? Omit<P[0], 'success' | 'fail' | 'complete'>
: never
// 提取微信API的返回值类型
type ExtractWxApiResult<T> = T extends WxApiFn<any, infer R> ? R : never
/**
* 微信API Promise化封装,带完整类型推断
* @param api 微信原生API
* @returns Promise化的API
*/
export function promisify<T extends WxApiFn<any[], any>>(api: T) {
return (params: ExtractWxApiParams<T>): Promise<ExtractWxApiResult<T>> => {
return new Promise((resolve, reject) => {
api({
...params,
success: resolve,
fail: reject
})
})
}
}
// 全局封装常用的微信API
export const wxApi = {
request: promisify(wx.request),
getLocation: promisify(wx.getLocation),
chooseAddress: promisify(wx.chooseAddress),
uploadFile: promisify(wx.uploadFile),
downloadFile: promisify(wx.downloadFile)
}
使用示例
typescript
运行
import { wxApi } from '@/utils/promisify'
// 有完整的类型提示,入参和返回值都有类型约束,写错参数会直接报错
const res = await wxApi.request({
url: 'https://api.example.com/order/list',
method: 'GET',
data: { page: 1 }
})
// res.data 有完整的类型推断,不再是any
console.log(res.data)
坑 3:前端与云函数的共享类型不同步
问题描述
小程序前端和云函数都使用 TS 开发,但是共享的类型(比如订单状态枚举、订单类型、用户类型)需要在两边重复定义,很容易出现修改了前端的类型,忘记修改云函数的类型,导致类型不一致,引发线上 bug。
解决方案
建立共享类型目录,通过微信开发者工具的构建 npm,实现类型的一次定义,两端共用,步骤如下:
- 在项目根目录创建
types文件夹,存放共享类型定义:
plaintext
├── types/
│ ├── order.ts // 订单相关共享类型
│ ├── user.ts // 用户相关共享类型
│ ├── service.ts // 服务相关共享类型
│ └── index.ts // 类型导出入口
├── miniprogram/ // 小程序前端目录
└── cloudfunctions/ // 云函数目录
- 在
types/index.ts中导出所有共享类型:
typescript
运行
export * from './order'
export * from './user'
export * from './service'
- 在
package.json中配置模块入口,让前端和云函数都能引用:
json
{
"name": "pet-miniprogram-types",
"version": "1.0.0",
"main": "types/index.ts",
"types": "types/index.ts"
}
- 前端和云函数中直接引用,无需重复定义:
typescript
运行
// 前端页面中引用
import { OrderStatus, OrderItem } from 'pet-miniprogram-types'
// 云函数中引用
import { OrderStatus, PayStatus } from 'pet-miniprogram-types'
- 每次修改共享类型后,执行「构建 npm」,即可同步到所有使用的地方,保证类型完全一致。
坑 4:App.globalData 类型缺失
问题描述
小程序的全局数据getApp().globalData默认是any类型,没有任何类型提示,写错字段、赋值类型错误都不会被 TS 检查到,很容易出现线上 bug。
解决方案
扩展全局 App 类型,给 globalData 加上完整的类型约束,步骤如下:
- 在
typings/index.d.ts中扩展 App 类型:
typescript
运行
// 定义全局globalData的类型
interface IGlobalData {
userInfo: {
nickName: string;
avatarUrl: string;
identity: 'user' | 'server' | 'admin';
} | null;
token: string;
systemInfo: WechatMiniprogram.SystemInfo | null;
currentAddress: {
address: string;
latitude: number;
longitude: number;
} | null;
}
// 扩展App的类型定义
declare namespace WechatMiniprogram {
interface AppOptions {
globalData: IGlobalData;
}
interface AppInstance {
globalData: IGlobalData;
}
// 重写getApp方法的返回类型
function getApp<T extends AppInstance = AppInstance>(): T;
}
- 在
app.ts中定义 globalData,严格遵循类型约束:
typescript
运行
App({
globalData: {
userInfo: null,
token: '',
systemInfo: null,
currentAddress: null
},
onLaunch() {
// 获取系统信息,有完整的类型提示
this.globalData.systemInfo = wx.getSystemInfoSync()
}
})
- 在页面中使用,有完整的类型提示:
typescript
运行
// 类型安全,globalData的所有字段都有类型提示,写错会直接报错
const app = getApp()
console.log(app.globalData.userInfo?.identity)
坑 5:分包加载下的类型引用报错
问题描述
小程序采用分包加载后,分包中的 TS 文件引用主包的类型、工具函数时,会出现「找不到模块」的类型报错,虽然编译能正常运行,但是 TS 会一直标红,影响开发体验。
解决方案
在tsconfig.json中配置路径别名,解决分包引用的路径问题,核心配置如下:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
// 主包根目录别名,分包中用@/引用主包的文件
"@/*": ["./miniprogram/*"],
"@/utils/*": ["./miniprogram/utils/*"],
"@/typings/*": ["./typings/*"],
"@/types/*": ["./types/*"]
}
}
}
分包中使用示例
typescript
运行
// 分包中的页面,引用主包的工具函数和类型,不会再报找不到模块的错误
import { wxApi } from '@/utils/promisify'
import { OrderItem } from '@/types/order'
三、提升开发效率的 TS 实战技巧
技巧 1:全链路类型安全,从数据库到前端的类型贯通
在宠物上门小程序的开发中,我们实现了从云数据库→云函数→前端的全链路类型安全,彻底杜绝了字段写错、类型不匹配的问题,核心实现如下:
- 先定义数据库文档的 TS 类型,和数据库集合的字段完全对应:
typescript
运行
// types/order.ts
import { OrderStatus, PayStatus } from './enums'
// 订单表文档类型,和数据库字段完全一致
export interface OrderDoc {
_id: string;
order_id: string;
user_id: string;
server_id: string;
service_id: string;
service_name: string;
order_amount: number;
pay_amount: number;
pay_status: PayStatus;
order_status: OrderStatus;
appointment_date: string;
appointment_time_slot: string;
address: {
name: string;
phone: string;
address: string;
latitude: number;
longitude: number;
};
create_time: number;
pay_time: number;
accept_time: number;
finish_time: number;
update_time: number;
}
// 订单列表项返回类型
export type OrderListItem = Pick<
OrderDoc,
'order_id' | 'service_name' | 'order_amount' | 'order_status' | 'create_time' | 'appointment_date'
>
// 订单详情返回类型
export type OrderDetail = OrderDoc
- 云函数中使用该类型,严格约束返回值:
typescript
运行
// 云函数:order/getOrderList
import { OrderListItem, OrderStatus } from 'pet-miniprogram-types'
exports.main = async (event, context) => {
const { page = 1, pageSize = 10 } = event
const openId = cloud.getWXContext().OPENID
// 查询订单,返回值严格遵循OrderListItem类型
const res = await db.collection('orders').where({
user_id: openId
}).skip((page - 1) * pageSize).limit(pageSize).get()
const orderList: OrderListItem[] = res.data
return {
errcode: 0,
errmsg: 'success',
data: {
orderList,
total: res.data.length
}
}
}
- 前端调用云函数,使用相同的类型约束,实现全链路类型安全:
typescript
运行
import { OrderListItem } from '@/types/order'
async getOrderList() {
const res = await wx.cloud.callFunction({
name: 'order/getOrderList',
data: { page: this.data.page }
})
if (res.result.errcode === 0) {
// 严格类型约束,字段不匹配会直接报错
const orderList: OrderListItem[] = res.result.data.orderList
this.setData({ orderList })
}
}
技巧 2:封装带泛型的云函数调用工具,统一类型约束
我们封装了通用的云函数调用工具,通过泛型指定入参和出参的类型,避免每次调用都写重复的类型断言,同时统一处理错误,核心代码如下:
typescript
运行
// utils/cloud.ts
/**
* 云函数调用通用工具,带泛型类型约束
* @param name 云函数名称
* @param data 入参
* @returns Promise<出参>
*/
export async function callCloudFunction<T = any, R = any>(
name: string,
data: T
): Promise<{
errcode: number;
errmsg: string;
data: R;
}> {
try {
const res = await wx.cloud.callFunction({
name,
data
})
return res.result
} catch (err) {
console.error(`云函数[${name}]调用失败`, err)
wx.showToast({
title: '网络异常,请重试',
icon: 'none'
})
return {
errcode: -1,
errmsg: '网络异常,请重试',
data: {} as R
}
}
}
使用示例
typescript
运行
import { callCloudFunction } from '@/utils/cloud'
import { OrderListItem } from '@/types/order'
// 定义入参类型
interface GetOrderListParams {
page: number;
pageSize?: number;
order_status?: string;
}
// 定义出参类型
interface GetOrderListResult {
orderList: OrderListItem[];
total: number;
hasMore: boolean;
}
// 调用云函数,有完整的入参和出参类型提示,参数写错会直接报错
const res = await callCloudFunction<GetOrderListParams, GetOrderListResult>(
'order/getOrderList',
{ page: 1, pageSize: 10 }
)
// res.data 有完整的类型推断,不再是any
if (res.errcode === 0) {
this.setData({
orderList: res.data.orderList,
total: res.data.total
})
}
技巧 3:用枚举和联合类型替代魔法字符串,杜绝低级 bug
在业务开发中,我们会遇到大量的状态、类型字段,很多开发者习惯直接写硬编码的字符串(魔法字符串),比如order_status === 'pending_pay',一旦写错字符串,不会有任何报错,但是会导致业务逻辑异常,很难排查。
使用 TS 的枚举和联合类型,可以在编译阶段就发现这类错误,彻底杜绝低级 bug,示例如下:
typescript
运行
// 用枚举定义订单状态,禁止硬编码字符串
export enum OrderStatus {
PENDING_PAY = 'pending_pay',
PENDING_ACCEPT = 'pending_accept',
PENDING_SERVICE = 'pending_service',
COMPLETED = 'completed',
CANCELED = 'canceled'
}
// 用联合类型定义用户身份
export type UserIdentity = 'user' | 'server' | 'admin'
// 业务代码中使用,编译时检查,写错会直接标红报错
if (order.order_status === OrderStatus.PENDING_PAY) {
// 待支付订单逻辑
}
// 身份校验,只能是联合类型中定义的三个值,其他值会直接报错
const userIdentity: UserIdentity = 'user'
技巧 4:开启严格模式,强制类型检查,拒绝 AnyScript
很多开发者为了省事,关闭了 TS 的严格模式,甚至给所有报错的地方加上// @ts-ignore,最终把 TS 写成了 AnyScript,完全失去了 TS 的类型安全优势。
我们强烈建议开启 TS 的严格模式,在tsconfig.json中配置:
json
{
"compilerOptions": {
"strict": true, // 开启所有严格检查
"noImplicitAny": true, // 禁止隐式any类型
"noImplicitThis": true, // 禁止this隐式any类型
"strictNullChecks": true, // 严格空值检查
"strictFunctionTypes": true // 严格函数类型检查
}
}
开启严格模式后,TS 会强制检查所有类型,不允许隐式的any类型,提前发现所有可能的类型错误,虽然前期会增加一些开发成本,但是长期来看,能极大减少线上 bug,提升代码的可维护性,尤其是多人协作的项目,优势非常明显。
总结
TypeScript 开发微信小程序的核心价值,是通过编译时的类型检查,替代运行时的错误,提前发现业务代码中的问题,提升开发效率,减少线上 bug。
本文讲解的避坑方案和实战技巧,全部经过线上项目验证,能解决小程序 TS 开发中 90% 以上的问题,帮助大家真正发挥 TS 的类型安全优势,而不是把 TS 写成 AnyScript。
下一篇文章,我会详细拆解宠物上门小程序的核心功能 ——《微信小程序时间段限流预约功能的实现方案》,完整讲解前端交互、后端库存校验、并发防超卖的全流程代码实现,欢迎点赞收藏关注。
更多推荐
所有评论(0)