likes
comments
collection
share

Provide/Inject + TypeScript 使用

作者站长头像
站长
· 阅读数 26

翻译来自: logaretm.com/blog/2020-1…

本文是一篇关于 provide/inject TypeScript 用法介绍的简短文章,在 Vue3 以及 Vue 2 的 @vue/composition-api 都支持 provide/inject TypeScript 用法。

Provide 类型安全

刚开始在组合 API 中使用 provide/inject 的时候,我写的代码如下:

import { inject } from 'vue';
import { Product } from '@/types';
export default { 
  setup() { 
    const product = inject('product') as Product; 
  },
};

这样写,你发现问题了吗?

在使用 TypeScript 的时候,如果不知道怎么让 TypeScript 理解正在处理的类型,我会使用 as 关键词作为了一种逃避手段。即使我很少使用 as, 但是我仍然尽量避免去使用 as。

不久前,关于这个话题我还发布了 tweeter:我在一个 TypeScript 产品应用中使用了组合API,需要为provide/inject 提供类型信息,我打算自己来做,但发现 Vue 已经有一个名为InjectionKey<T> 的工具类型,它正是我需要的。

Provide/Inject + TypeScript 使用

这意味着你需要有一个特殊的常量文件来保存 Injectable 键,然后你可以使用 InjectionKey<T> 来创建包含注入属性类型信息的 Symbol

// types.ts
interface Product { name: string; price: number;}

// symbols.ts
import { InjectionKey } from 'vue';
import { Product } from '@/types';
const ProductKey: InjectionKey<Product> = Symbol('Product');

InjectionKey<T> 的优点在于它可以双向工作。它提供了类型安全来提供,这意味着如果你试图用那个键提供一个不兼容的值,TypeScript会报错:

import { provide } from 'vue';
import { ProductKey } from '@/symbols';
// ⛔️ Argument of type 'string' is not assignable to ...
provide(ProductKey, 'this will not work');
// ✅
provide(ProductKey, {
  name: 'Amazing T-Shirt',
  price: 100,
});

在接收端,你的 inject 也会被正确输入:

import { inject } from 'vue';
import { ProductKey } from '@/symbols';
const product = inject(ProductKey); // typed as Product or undefined

需要注意的一点是,inject 函数将生成与 undefined 结合的解析类型。这是因为有可能组件并没有注入。这取决于你想如何处理它。

要消除 undefined,需要向 inject 函数传递一个默认值。这里很酷的是,默认值也是类型检查的:

import { inject } from 'vue';
import { ProductKey } from '@/symbols';
// ⛔️ Argument of type 'string' is not assignable to ...
const product = inject(ProductKey, 'nope');
// ✅ Type checks out
const product = inject(ProductKey, { name: '', price: 0 });

Provide 响应式的值

虽然你可以 provide 普通的值类型,但它们并不常用,因为我们往往需要对这些值的更改作出反应。还可以使用泛型类型创建 reactive 注入。

对于使用 ref 创建的响应式引用,你可以使用泛型 Ref 类型来输入你的 InjectionKey,所以它是一个嵌套泛型类型:

// types.ts
interface Product {
  name: string;
  price: number;
}
// symbols.ts
import { InjectionKey, Ref } from 'vue';
import { Product } from '@/types';
const ProductKey: InjectionKey<Ref<Product>> = Symbol('Product');

现在,当你通过 ProductKey获取组件 inject 内容时,将得到具有 Ref 类型或 undefined,就像我们之前讨论的那样:

import { inject } from 'vue';
import { ProductKey } from '@/symbols';
const product = inject(ProductKey); // typed as Ref<Product> | undefined
product?.value; // typed as Product

处理 undefined

我已经提到了如何处理用普通值解析 undefined 的问题,但是对于复杂对象和响应性对象,你无法提供一个安全的默认值。

在我们的示例中,我们尝试解决一个 Product 上下文对象,如果该注入不存在,那么可能存在一个更严重的潜在问题,如果没有找到注入,可能会出现错误。

Vue 默认显示一个警告如果不解决注射,Vue 可以选择抛出错误如果没有找到注射但 Vue 不能假设是否需要注射,这是由你来理解解决注入和 undefined 的值。

对于可选的注入属性,只要提供一个默认值,或者如果不是那么重要,你也可以使用可选的链接操作符:

import { inject } from 'vue';
import { CurrencyKey } from '@/symbols';
const currency = inject(CurrencyKey, ref('$'));
currency.value; // no undefined
// or
const currency = inject(CurrencyKey);
currency?.value;

但是对于像我们的 Product 类型这样的需求,我们可以做一些像这样简单的事情:

import { inject } from 'vue';
import { ProductKey } from '@/symbols';
const product = inject(ProductKey);
if (!product) {
  throw new Error(`Could not resolve ${ProductKey.description}`);
}
product.value; // typed as `Ref<Product>`

抛出错误是利用 TypeScript 类型检查特性的一种方法。因为我们在早期处理了 undefined 的组件,所以如果实际使用的时候不添加 product类型 ,代码就无法正常运行到最后一行。

为了更可重用,让我们创建一个名为 injectStrict 的函数,它为我们做所有这些:

function injectStrict<T>(key: InjectionKey<T>, fallback?: T) {
  const resolved = inject(key, fallback);
  if (!resolved) {
    throw new Error(`Could not resolve ${key.description}`);
  }
  return resolved;
}

现在,你可以直接使用它而不是 inject,你将以模块化的方式获得同样的安全性,而不必处理讨厌的 undefined:

import { injectStrict } from '@/utils';

const product = injectStrict(ProductKey);

product.value; // typed as `Product`

总结

我认为 provide/inject 会越来越流行,特别是随着 composition API的出现,了解它们的TypeScript 功能会让你的代码更容易维护,使用起来也更安全。

Thanks for reading! 👋