最近博主在开发的过程中遇到了这样的一个类型问题,借此机会分享一下。
Q
首先,代码如下:
type Api_A = (arg: number) => number;
type Api_B = (arg: string) => string;
const api_a: Api_A = (_) => _;
const api_b: Api_B = (_) => _;
enum SourceType {
Use_A,
Use_B,
}
const ApiMap = {
[SourceType.Use_A]: [api_a],
[SourceType.Use_B]: [api_b],
} as const;
export const handleClick = (type: SourceType) => {
const [api] = ApiMap[type];
api(type === SourceType.Use_A ? 1 : 'str'); // error
};
这是一个通过 type
表示来在 ApiMap
中选择不同类型的 api
调用的场景。
这段代码看起来是正常且合理的,可是当我们码到 api(type === SourceType.Use_A ? 1 : 'str')
类型推断却除了问题,来看下错误提示:
'string|number' is not assignable to 'never'
从这段错误提示中我们不难看出 api
的类型被推断成了 (arg: never) => number | string
。可是按理来说 api 的类型应该是 Api_A | Api_B
。事实上知道调用之前他一直是如此:
那为什么在调用点它的类型发生变化了呢,好了梳理到了这里也就引出了本文的主题:这是为什么?
A
接下来是解答的部分,想搞清楚这个问题,要从两部分入手:
第一部分 typescript
对你的代码进行的是静态分析,它并不能拿到运行时的变量类型上下文,也就是说它并不知道你的代码逻辑中 api
的类型和 type
关联在了一起。
既然他不能判断你在调用 api
这个函数的时候它被推断成了联合类型中的哪一种,并且你没手动的进行断言,于是为了同时兼容联合类型中的两个成员,api
的类型被推断为了二者的超类。
然后便是第二部分,为何 (arg: number) => number | (arg: string) => string
的超类是 (arg: never) => string | number
?
如果你了解 typescript
的一些细节,这里其实不难理解,首先当一个函数类型想兼容另一个函数类型时,被兼容的函数参数必须是逆变的,而返回值是协变的。
注:如果你目前不理解逆变和协变的概念,那你可以把逆变当成超类->子类,协变则是子类->超类。看完这篇之后记得自行
根据这条规则 api
的参数类型需要是 number
和 string
的共同子类,可这两个基础类型的共同子类是什么呢?答案是:
type _T = string & number; // never
现在我们就明白这个 arg: never
是怎么来的了。相对的返回值要好理解一些 number
和 string
的共同超类就是 string | number
。
到这里这个问题就被我们分析透了。至于解决的办法也很简单,就是手动进行类型断言:
export const handleClick = (type: SourceType) => {
const [api] = ApiMap[type];
if (type === SourceType.Use_A) {
(api as Api_A)(1);
} else {
(api as Api_B)("str");
}
};
除此之外我们还可以对向下转型进行封装,这样写起来优雅一些:
export const handleClick = (type: SourceType) => {
const [api] = ApiMap[type];
const assertApi_A = () => api as Api_A;
const assertApi_B = () => api as Api_B;
if (type === SourceType.Use_A) {
assertApi_A()(1);
} else {
assertApi_B()("str");
}
};