TypeScript 开发微信小程序的避坑指南与实战技巧

本文已收录于 CSDN 专栏《微信小程序云开发实战:宠物上门预约系统全流程》,原创不易,欢迎点赞、收藏、关注。

前言

TypeScript 早已成为前端开发的工业级标准,微信小程序官方也早已原生支持 TypeScript(以下简称 TS)开发。但在实际的小程序开发中,尤其是结合微信原生 API、云开发、自定义组件的场景下,我们会遇到大量的类型丢失、适配错误、this 指向异常等坑,很多开发者最终把 TS 写成了 AnyScript,完全失去了 TS 的类型安全优势。

本文基于宠物上门预约小程序的完整 TS 实战经验,整理了小程序 TS 开发中高频踩坑的解决方案,以及能显著提升开发效率的实战技巧,所有内容均经过线上项目验证,可直接落地使用。

一、微信小程序 TS 开发环境基础搭建与配置

1.1 项目初始化

全新项目

直接在微信开发者工具中,新建项目时选择「不使用云服务」/「微信云开发」,语言选择「TypeScript」,即可生成官方的 TS 模板项目,开箱即用。

现有 JS 项目升级 TS
  1. 在项目根目录创建tsconfig.json配置文件
  2. 安装微信小程序类型定义包:npm install miniprogram-api-typings -D
  3. .js文件后缀改为.ts,逐步补充类型定义
  4. 开启微信开发者工具的「增强编译」、「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.xxxthis.methods.xxx全部是any类型,没有任何类型提示,甚至会报类型错误。

错误示例

typescript

运行

Page({
  data: {
    orderList: [],
    page: 1
  },
  onLoad() {
    // 这里this.data.orderList 是any类型,没有类型提示,写错字段也不会报错
    console.log(this.data.orderList)
  }
})
解决方案

通过扩展微信小程序的命名空间,给 Page/Component 加上泛型约束,实现完整的类型推断,步骤如下:

  1. 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
}
  1. 页面中使用,实现完整的类型推断:

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,实现类型的一次定义,两端共用,步骤如下:

  1. 在项目根目录创建types文件夹,存放共享类型定义:

plaintext

├── types/
│   ├── order.ts      // 订单相关共享类型
│   ├── user.ts       // 用户相关共享类型
│   ├── service.ts    // 服务相关共享类型
│   └── index.ts      // 类型导出入口
├── miniprogram/      // 小程序前端目录
└── cloudfunctions/   // 云函数目录
  1. types/index.ts中导出所有共享类型:

typescript

运行

export * from './order'
export * from './user'
export * from './service'
  1. package.json中配置模块入口,让前端和云函数都能引用:

json

{
  "name": "pet-miniprogram-types",
  "version": "1.0.0",
  "main": "types/index.ts",
  "types": "types/index.ts"
}
  1. 前端和云函数中直接引用,无需重复定义:

typescript

运行

// 前端页面中引用
import { OrderStatus, OrderItem } from 'pet-miniprogram-types'

// 云函数中引用
import { OrderStatus, PayStatus } from 'pet-miniprogram-types'
  1. 每次修改共享类型后,执行「构建 npm」,即可同步到所有使用的地方,保证类型完全一致。

坑 4:App.globalData 类型缺失

问题描述

小程序的全局数据getApp().globalData默认是any类型,没有任何类型提示,写错字段、赋值类型错误都不会被 TS 检查到,很容易出现线上 bug。

解决方案

扩展全局 App 类型,给 globalData 加上完整的类型约束,步骤如下:

  1. 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;
}
  1. app.ts中定义 globalData,严格遵循类型约束:

typescript

运行

App({
  globalData: {
    userInfo: null,
    token: '',
    systemInfo: null,
    currentAddress: null
  },

  onLaunch() {
    // 获取系统信息,有完整的类型提示
    this.globalData.systemInfo = wx.getSystemInfoSync()
  }
})
  1. 在页面中使用,有完整的类型提示:

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:全链路类型安全,从数据库到前端的类型贯通

在宠物上门小程序的开发中,我们实现了从云数据库→云函数→前端的全链路类型安全,彻底杜绝了字段写错、类型不匹配的问题,核心实现如下:

  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
  1. 云函数中使用该类型,严格约束返回值:

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
    }
  }
}
  1. 前端调用云函数,使用相同的类型约束,实现全链路类型安全:

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。

下一篇文章,我会详细拆解宠物上门小程序的核心功能 ——《微信小程序时间段限流预约功能的实现方案》,完整讲解前端交互、后端库存校验、并发防超卖的全流程代码实现,欢迎点赞收藏关注。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐