Chapter 08

TypeScript 深入

Pinia 是 TS 一等公民——理解自动推断的边界、如何显式标注复杂 state、抽取 Store 类型、泛型 store 工厂。

8.1 核心概念

自动类型推断
Pinia 从 state/getters/actions 的实现自动推断出 store 的完整类型,无需辅助泛型。
state 类型标注
初值为 null/空数组时需用 as 断言,否则会被推断成 null/never[]
getter 返回类型
引用其它 getter(用 this)时应显式标注返回类型,避免循环推断报错。
StoreGeneric / 类型工具
Pinia 导出 ReturnType<typeof useXxxStore> 等模式,用来标注函数参数中的 store。
PiniaCustomProperties
用于声明插件注入属性的接口,让扩展属性获得类型提示。
接口优先
把 state 抽成 interface 再作为 state 返回类型,是复杂 store 的推荐实践。

8.2 自动推断已经足够

绝大多数情况下什么都不用标注,Pinia 已推断出正确类型:

export const useCounterStore = defineStore("counter", {
  state: () => ({ count: 0, name: "pinia" }),
  getters: {
    doubled: (state) => state.count * 2,   // number(推断)
  },
  actions: {
    setName(n: string) { this.name = n; },
  },
});

const s = useCounterStore();
s.count;      // number
s.doubled;    // number
s.setName(1);  // ❌ TS 报错:期望 string

8.3 为 state 显式标注类型

当初值无法推断出想要的类型时(如 null、空数组),用 as 或接口标注:

interface User { id: number; name: string; }

interface UserState {
  currentUser: User | null;
  list: User[];
}

export const useUserStore = defineStore("user", {
  // 用接口约束整个 state 形状
  state: (): UserState => ({
    currentUser: null,
    list: [],
  }),
});
不标注的坑

state: () => ({ user: null }) 会把 user 推断成 null 类型,之后赋值对象就报错;list: [] 会被推断成 never[]。给 state 一个 interface 返回类型是最稳妥的做法。

8.4 getter 显式返回类型

getters: {
  total: (state) => state.list.length,     // 自动推断 number

  // 引用其它 getter → 用 this → 标注返回类型
  summary(): string {
    return `共 ${this.total} 人`;
  },
},

8.5 组合式 Store 的类型

export const useUserStore = defineStore("user", () => {
  // 泛型 ref 明确类型
  const currentUser = ref<User | null>(null);
  const list = ref<User[]>([]);

  const total = computed(() => list.value.length);

  function add(u: User) {
    list.value.push(u);
  }

  return { currentUser, list, total, add };
});

8.6 抽取 Store 类型

要在工具函数或组件 props 里接收一个 store,用 ReturnType 提取它的类型:

// 提取 store 实例类型
type UserStore = ReturnType<typeof useUserStore>;

// 函数接收 store 作为参数
function logUser(store: UserStore) {
  console.log(store.currentUser?.name);
}

8.7 泛型 Store 工厂

需要为多种实体生成结构相同的 store(如列表 CRUD)时,可写一个泛型工厂:

function createListStore<T extends { id: number }>(name: string) {
  return defineStore(name, () => {
    const items = ref<T[]>([]);
    function add(item: T) { items.value.push(item); }
    function removeById(id: number) {
      items.value = items.value.filter((i) => i.id !== id);
    }
    return { items, add, removeById };
  });
}

// 使用
export const useProductStore = createListStore<Product>("products");

8.8 类型安全最佳实践

实践理由
state 用 interface 标注返回类型避免 null/never[] 推断坑
getter 引用 this 时标注返回类型防止循环推断报错
用 ReturnType 抽取 store 类型函数/props 传递 store 更安全
插件属性用 PiniaCustomProperties 声明获得类型提示
开启 strict / noImplicitAny让 Pinia 推断发挥最大价值
对比 Vuex

Vuex 要给 store 写一堆 ModuleTreeActionContext 泛型才能勉强类型化。Pinia 几乎零配置——这正是它取代 Vuex 的最大理由之一。

8.9 小结与下一步