我希望构建一个函数,可以在联合上执行类型缩小,该联合在运行时充当判别联合,但在TypeScript中不是判别联合类型。
我目前有这个(简化)代码,但是if
语句中的props
总是具有WidgetPropsSet
类型:
if (props.kind === "CheckboxInput") {
console.log("We need to use", props, "here as its narrowed `CheckboxInput` type");
}
if (props.kind === "DropdownInput") {
console.log("We need to use", props, "here as its narrowed `DropdownInput` type");
}
// ... 16 more
(这段代码的非简化版本是一个Svelte模板,因此上面显示的结构不能真正重新设计)
我认为我可以用函数(TS伪代码)替换if语句内的props.kind === "
discriminator
"
:
function narrowWidgetProps(props: WidgetPropsSet, kind: WidgetPropsNames): a-concrete-variant-from-WidgetPropsSet | undefined {
if (props.kind === kind) return a-concrete-variant-from-WidgetPropsSet;
else return undefined;
}
我如何在适当的TypeScript中编写这个伪代码函数来执行所需的窄化?
对于上下文,这些是我正在处理的联合变体:
abstract class WidgetProps {
kind!: string;
}
class CheckboxInput extends WidgetProps {
checked!: boolean;
disabled!: boolean;
// ...
}
class DropdownInput extends WidgetProps {
entries!: string[];
selectedIndex!: number | undefined;
// ...
}
这里是实际的联合,它需要用{ value, name }[]
格式,这样widgetSubTypes
就可以提供给需要这种格式的第三方代码。
const widgetSubTypes = [
{ value: CheckboxInput, name: "CheckboxInput" },
{ value: DropdownInput, name: "DropdownInput" },
// ...
] as const;
// Evaluated type:
// const widgetSubTypes: readonly [{
// readonly value: typeof CheckboxInput;
// readonly name: "CheckboxInput";
// }, {
// readonly value: typeof DropdownInput;
// readonly name: "DropdownInput";
// }, ... 16 more ..., {
// ...;
// }]
type WidgetPropsSet = InstanceType<typeof widgetSubTypes[number]["value"]>;
// Evaluated type:
// type WidgetPropsSet = CheckboxInput | DropdownInput | ... 16 more
type WidgetPropsNames = typeof widgetSubTypes[number]["name"];
// Evaluated type:
// type WidgetPropsNames = "CheckboxInput" | "DropdownInput" | ... 16 more
下面是一个TypeScript Playground的链接,可以通过编辑顶部的单个函数来解决这个问题。
理想情况下,联合成员应该具有文字类型的kind
属性,这样它将是一个真正的区分联合,您将自动获得所需的缩小,而不需要辅助函数:
class CheckboxInput extends WidgetProps {
declare checked: boolean;
declare disabled: boolean;
declare kind: "CheckboxInput" // <-- literal type
}
class DropdownInput extends WidgetProps {
declare entries: string[];
declare selectedIndex: number | undefined;
declare kind: "DropdownInput" // <-- literal type
}
declare const props: CheckboxInput | DropdownInput;
if (props.kind === "CheckboxInput") {
props.checked // props is automatically narrowed to CheckboxInput
}
if (props.kind === "DropdownInput") {
props.entries // props is automatically narrowed to DropdownInput
}
但是如果由于某种原因你不能这样做,并且需要在每个子类中单独留下kind
属性类型,那么我们可以这样写你的narrowWidgetProps()
函数:
const widgetSubTypes = [
{ value: CheckboxInput, name: "CheckboxInput" },
{ value: DropdownInput, name: "DropdownInput" },
] as const;
type WidgetSubTypes = typeof widgetSubTypes[number];
type WidgetKindMap = { [T in WidgetSubTypes as T["name"]]: InstanceType<T["value"]> };
/* type WidgetKindMap = {
CheckboxInput: CheckboxInput;
DropdownInput: DropdownInput;
} */
type WidgetPropsNames = keyof WidgetKindMap
type WidgetPropsSet = WidgetKindMap[WidgetPropsNames]
function narrowWidgetProps<K extends WidgetPropsNames>(props: WidgetPropsSet, kind: K) {
if (props.kind === kind) return props as WidgetKindMap[K]
else return undefined;
}
我使用widgetSubTypes
的定义来生成WidgetKindMap
,这是一个助手类型,其键是字面kind
值(它们一起构成WidgetPropsNames
联合),其值是WidgetProps
的相应子类的实例类型(它们一起构成WidgetPropsSet
联合)。
则narrowWidgetProps()
函数在kind
参数的类型K
中是泛型的。如果这与props
参数的kind
属性不匹配,则返回undefined
。如果doeS匹配,则断言props
实际上属于WidgetKindMap[K]
类型(一种索引访问类型,即"WidgetKindMap
的值类型对应于K
类型的键")。
我们需要断言as WidgetKindMap[K]
,因为编译器没有理由相信匹配kind
属性可以将props
缩小为该类型。如果它确实这样认为,那么WidgetPropsSet
就已经是一个受歧视的工会,这种努力就没有必要了,如上所述。
无论如何,让我们测试一下:
declare const props: WidgetPropsSet;
const narrowedCheckboxInput = narrowWidgetProps(props, "CheckboxInput");
if (narrowedCheckboxInput) {
// const narrowedCheckboxInput: CheckboxInput | undefined
narrowedCheckboxInput.checked
}
const narrowedDropdownInput = narrowWidgetProps(props, "DropdownInput");
if (narrowedDropdownInput) {
// const narrowedDropdownInput: DropdownInput | undefined
narrowedDropdownInput.entries
}
看起来不错。narrowWidgetProps()
的返回类型取决于你想要的kind
参数。
Playground链接到代码