我正在使用打字稿 4.6
在我的项目中,我有一个名为FooterFormElement
的类型,它具有判别属性type
type FooterFormElement = {type:"number",...}|{type:"button",...};
为了实例化这些元素,我有一个名为footerFormElementDictionary
的字典:
footerFormElementDictionary:{
[key in FooterFormElement["type"]]: (element: FooterFormElement & { type: key }) => HTMLElement
}
问题是,当我尝试使用此字典时,如以下示例所示
this.footerFormElementDictionary[item.type](item)
我收到错误:Argument of type 'FooterFormElement' is not assignable to parameter of type 'never'
似乎打字稿编译器无法"展开"不同值的type
流分析。
我可以解决此问题的任何方法(除了丑陋的非类型安全解决方法)。
下面是一个缩短的最小可重现示例:
type FooterFormData = { [k: string]: number };
type FooterFormElement = {
type: "button"; Action: (data: FooterFormData) => void;
} | {
type: "number"; label: string; Action?: (value: number) => void;
}
declare const footerFormElementDictionary: {
[K in FooterFormElement["type"]]: (
element: FooterFormElement & { type: K },
formData: FooterFormData
) => HTMLElement;
}
function MakeFooterForm(elements: FooterFormElement[]): HTMLElement[] {
let formData: FooterFormData = {};
return elements.map((item) =>
footerFormElementDictionary[item.type](item, formData)
);
}
下面是一个可重现的示例,展示了该问题。
为了具体和简洁起见,我将指定您的类型,与外部链接中的类型略有不同:
type FooterFormElement =
{ type: "button"; buttonStuff: string } |
{ type: "number"; numberStuff: string };
declare const footerFormElementDictionary: {
[K in FooterFormElement["type"]]: (
element: Extract<FooterFormElement, { type: K }>
) => HTMLElement;
}
似乎您应该能够抓住任何FooterFormElement
类型的item
,并调用footerFormElementDictionary[item.type](item)
。 但是,如您所见,编译器无法验证这是否是类型安全的:
function problem(item: FooterFormElement) {
const fn = footerFormElementDictionary[item.type];
return fn(item); // error!
// -----> ~~~~
// Argument of type 'FooterFormElement' is not assignable to parameter of type 'never'.
// The intersection '{ type: "number"; numberStuff: string; } &
// { type: "button"; buttonStuff: string; }' // was reduced to 'never'
}
让我们使用 IntelliSense 查看编译器看到的fn
和item
类型:
/* const fn: (
(element: { type: "number"; numberStuff: string },
formData: FooterFormData) => HTMLElement
) | (
(element: { type: "button"; buttonStuff: string },
formData: FooterFormData) => HTMLElement
) */
/* (parameter) item:
{ type: "button"; buttonStuff: string; } |
{ type: "number"; numberStuff: string; } */
fn
的类型是函数类型的并集,item
的类型也是并集。 这些类型没有错,但它们对您的目的不够有用。 编译器将这些联合视为不相关。 编译器只知道fn
要么是接受类似"button"
输入的东西,要么是键入类似"number"
输入的东西,但它不知道是哪个。
因此,它唯一可以安全地接受的输入将是既"button"
又"number"
相似的东西。 这将是输入类型的交集。 由于输入类型是可区分并集的成员,因此两个不兼容成员的交集被简化为never
类型。
同样,编译器担心也许fn
会接受类似"number"
的输入,但item
结果会"button"
类似,反之亦然。 这不会发生,因为fn
与item
相关,但这种相关性并不表示在fn
和item
的类型中。编译器无法验证两个联合类型是否相关的此问题是 microsoft/TypeScript#30581 的主题。
在 TypeScript 4.6 之前,我会告诉你只使用类型断言来抑制错误,或者如果你真的关心类型安全而不是便利性,我会写一些冗余代码。 没有比这更好的了。
TypeScript 4.6 引入了一些在 microsoft/TypeScript#47109 中实现的索引访问改进,这允许我们重构代码以根据分发对象类型表示FooterFormElement
和footerFormElementDictionary
,以便有问题的函数调用可以作为泛型函数成功。
这是重构。 首先,我们采用原始FooterFormElement
并将其重构为键值映射类型,其中原始type
属性现在是键,其余属性是新的值类型:
interface FooterFormElementMap {
button: { buttonStuff: string },
number: { numberStuff: string }
}
然后我们可以将FooterFormElement
编写为分发对象类型,这是一个映射类型,您可以立即索引到该类型中以获取所有属性类型的并集:
type FooterFormElement<K extends keyof FooterFormElementMap =
keyof FooterFormElementMap> =
{ [P in K]: { type: P } & FooterFormElementMap[P] }[K];
如果你只是写FooterFormElement
你会得到一些与以前相同的联合类型:
type Test = FooterFormElement;
/* type Test =
({ type: "number"; } & { numberStuff: string; }) |
({ type: "button"; } & { buttonStuff: string; }) */
如果你写FooterFormElement<"button">
或FooterFormElement<"number">
,你只会得到你关心的工会成员。 这使我们可以将FooterFormElement<K>
编写为泛型类型。
最后,我们在FooterFormElementMap
键中根据每个K
的FooterFormElement<K>
重写footerFormElementDictionary
类型:
declare const footerFormElementDictionary: {
[K in keyof FooterFormElementMap]: (
element: FooterFormElement<K>
) => HTMLElement;
}
事实证明,这种表示形式很重要,并且允许编译器查看footerFormElementDictionary
中的属性与泛型K
的FooterFormElement
类型的值之间的关系。
最后,我们可以将之前有问题的函数转换为通用工作版本:
function solution<K extends keyof FooterFormElementMap>(item: FooterFormElement<K>) {
const fn: (element: FooterFormElement<K>) => HTMLElement =
footerFormElementDictionary[item.type];
// item: FooterFormElement<K>
return fn(item) // okay!
}
这是有效的,因为fn
被视为一个需要FooterFormElement<K>
的函数,而item
被视为一个FooterFormElement<K>
。 因此,如果你有一个FooterFormElement
数组,你可以将map()
与泛型回调一起使用,它就可以工作了:
function MakeFooterForm(elements: FooterFormElement[]): HTMLElement[] {
return elements.map(<K extends keyof FooterFormElementMap>(
item: FooterFormElement<K>) => footerFormElementDictionary[item.type](item) //
);
}
万岁!
为了清楚起见,如果我们将footerFormElementDictionary
的类型注释重写为结构上等效的版本,而不提及FooterFormElement<K>
,这一切都会中断:
declare const badDictionary: {
button: (element: { type: "button"; } & { buttonStuff: string; }) => HTMLElement;
number: (element: { type: "number"; } & { numberStuff: string; }) => HTMLElement;
}
function problemAgain<K extends keyof FooterFormElementMap>(item: FooterFormElement<K>) {
return badDictionary[item.type](item); // error!
// ---------------------------> ~~~~
// Argument is not assignable to parameter of type 'never'
}
如您所见,函数和参数类型之间的相关性是脆弱的,并且取决于它们类型定义之间的连接。 所以要小心。
操场链接到代码