第三篇: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.jsonjson{ 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二、高频踩坑指南与解决方案坑 1Page/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 Recordstring, any {}, TCustom extends Recordstring, any {} InstancePageTData, TCustom TCustom { data: TData } // 重写Page构造器的类型 function Page TData extends Recordstring, any {}, TCustom extends Recordstring, any {} ( options: PageOptionsTData, TCustom ThisTypePageInstanceTData, TCustom ): void }页面中使用实现完整的类型推断typescript运行// 定义页面data的类型 interface PageData { orderList: OrderItem[]; page: number; total: number; loading: boolean; } // 定义页面自定义方法的类型 interface PageCustom { getOrderList: (isRefresh?: boolean) Promisevoid; onRefresh: () void; } // 订单列表项类型 interface OrderItem { order_id: string; service_name: string; order_amount: number; order_status: string; create_time: number; } PagePageData, 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 Recordstring, any {}, TProperties extends Component.PropertyOption {}, TMethods extends Component.MethodOption {}, TCustom extends Recordstring, any {} ( options: Component.ComponentOptionsTData, TProperties, TMethods, TCustom ThisTypeComponent.InstanceTData, 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 WxApiFnT extends any[], R (...args: T) void // 提取微信API的参数类型排除success/fail/complete回调 type ExtractWxApiParamsT T extends WxApiFninfer P, any ? OmitP[0], success | fail | complete : never // 提取微信API的返回值类型 type ExtractWxApiResultT T extends WxApiFnany, infer R ? R : never /** * 微信API Promise化封装带完整类型推断 * param api 微信原生API * returns Promise化的API */ export function promisifyT extends WxApiFnany[], any(api: T) { return (params: ExtractWxApiParamsT): PromiseExtractWxApiResultT { 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」即可同步到所有使用的地方保证类型完全一致。坑 4App.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 getAppT 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 callCloudFunctionT 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 callCloudFunctionGetOrderListParams, 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。下一篇文章我会详细拆解宠物上门小程序的核心功能 ——《微信小程序时间段限流预约功能的实现方案》完整讲解前端交互、后端库存校验、并发防超卖的全流程代码实现欢迎点赞收藏关注。