TS函数,在给定类似区别键的值时执行联合窄化

  • 本文关键字:执行 区别 函数 TS typescript
  • 更新时间 :
  • 英文 :


我希望构建一个函数,可以在联合上执行类型缩小,该联合在运行时充当判别联合,但在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链接到代码

相关内容

最新更新