不幸的是,在TypeScrip中,generic个类型参数目前不受control flow analysis的影响.所以在case
号街区内
const getNextRoute = <F extends FORM_ID>(formId: F, state: StateType<F>) => {
switch (formId) {
case FORM_ID.STEP_1:
if (state?.a) { // error
return '/step-3';
}
return '/step-2';
default:
return '/';
}
};
formId
value的类型已经从通用类型参数F
缩窄到F & FORM_ID.STEP_1
,类型参数F
本身不受影响. 因此,编译器无法验证类型StateType<F>
的state
可以像StateType<FORM_ID.STEP_1>
一样处理,并且它失败了.
这似乎是一个奇怪的限制,但事实证明很难正确实现所需的行为.让我们想象一下最简单的情况;一个函数,如
function f<T extends string | number>(a: T, b: T) {
return (typeof a === "string") ? b.toUpperCase() : b.toFixed() // error!
// ~~~~~~~~~~~ <---> ~~~~~~~
}
编译器抱怨b
仍然是string | number
,因此不知道有toUpperCase
或toFixed
. 很明显,判断a
也应该影响b
,对吗?
不对.T
不一定是string
的子类型或number
的子类型;它也可以是union type.就像这样:
f(Math.random() < 0.99 ? "" : 0, 0); // okay
第一个参数是"" | 0
类型的,因此T
被推断为"" | 0
.这是可以接受的,因为"" | 0
是string | number
的子类型.然后判断第二个参数实参,果然,0
也是类型"" | 0
的有效值.如果T
是"" | 0
,那么a
很可能是string
,b
是number
,反之亦然.实际上,此调用有99%的机会在运行时调用0
上的toUpperCase()
并导致运行时错误.哎呀.
因此,任何将泛型类型参数与控制流分析结合在一起的更改都需要仔细考虑.有各种开放的特性请求来做到这一点;与您的示例代码最相关的是microsoft/TypeScript#33014和microsoft/TypeScript#27808.如果实现了这两种方法中的一种或两种,您的代码可能开始按原样工作.但就目前而言,它不是语言的一部分,您需要解决它.
到目前为止,最简单的解决方法是使用type assertion:
const getNextRoute = <F extends FORM_ID>
(
formId: F, state: StateType<F>
) => {
switch (formId) {
case FORM_ID.STEP_1:
if ((state as Step1FormValues)?.a) { // assert
return '/step-3';
}
return '/step-2';
default:
return '/';
}
};
在这里,我们不再试图让编译器遵循逻辑,而只是tell地让编译器了解正在发生的事情.这意味着我们必须更加小心,因为编译器可能不会注意到我们的断言是否不正确.
如果您do希望编译器遵循您的逻辑,那么您将需要重构以停止使用泛型函数,或者停止使用控制流分析.
一种方法是放弃泛型,以便可以使用控制流分析. 只有当你的函数实际上不是泛型的时候,这才是可行的,比如如果返回类型不依赖于输入. 由于无论如何输出都是string
,因此您应该能够重写调用签名,使其不是通用的. 这里有一种方法可以做到这一点:
type GetNextRouteParams =
{ [F in FORM_ID]: [formId: F, state: StateType<F>] }[FORM_ID];
/* type GetNextRouteParams =
[formId: FORM_ID.STEP_1, state: Step1FormValues] |
[formId: FORM_ID.STEP_2, state: Step2FormValues] |
[formId: FORM_ID.STEP_3, state: Step3FormValues]; */
const getNextRoute: (...args: GetNextRouteParams) => string =
(formId, state) => {
switch (formId) {
case FORM_ID.STEP_1:
if (state?.a) { // okay
return '/step-3';
}
return '/step-2';
default:
return '/';
}
};
如果判断GetNextRouteParams
的类型,您会发现它是tuple types的discriminated union,与您希望getNextRoute()
接受的参数集相对应.然后,我们将元组并集的rest parameter赋给getNextRoute
‘S调用签名,突然,实现工作了.
编译器知道它可以将formId
和state
参数视为destructured discriminated union,因此当判断判别式formId
时,state
的类型被相应地缩小.
如果您不能放弃泛型,那么您可以通过重构您的行为,从而将切换替换为索引到对象来追求放弃控制流缩小.这使您可以使用在microsoft/TypeScript#47109中实现的支持来执行看起来像是单个通用操作的操作.例如:
const getNextRoute = <F extends FORM_ID>(formId: F, state: StateType<F>) => {
const defaultCase = () => { return '/' };
const cases: { [F in FORM_ID]: (state: StateType<F>) => string } = {
[FORM_ID.STEP_1]: state => {
if (state?.a) {
return '/step-3'
}
return '/step2'
},
[FORM_ID.STEP_2]: defaultCase,
[FORM_ID.STEP_3]: defaultCase
}
return cases[formId](state);
};
在这里,我用一个名为cases
的对象替换了switch
/case
,该对象的键对应于formId
(类型为F
),其值是其参数对应于state
(类型为StateType<F>
)的函数.然后,通过判断单个代码块cases[formId](state)
,编译器理解这是允许的.
这是一个复杂的重构,可能会让大多数阅读它的人感到困惑.但是,如果您确实需要泛型(比如函数的返回类型也是SomeOtherType<F>
),并且仍然希望编译器遵循您的逻辑,这是目前我所知道的使其工作的唯一方法.
所以,这就对了.泛型和控制流分析不能很好地结合在一起;您可以使用其中一个,但不能使用另一个,或者干脆完全放弃并使用类型断言.
Playground link to code个