Typescript泛型联合类型

12

我正在尝试创建一个简单的开关函数,该函数需要一个参数,该参数必须是字符串和对象的联合类型,并且其键基于第一个参数的联合类型,并可以返回任何值。

export const mySwitch = <T extends string>(value: T, possibilities: {[key in T]: any}): any => {
    return possibilities[value];
};

典型用法是

let option: "val1" | "val2" | "val3" = "val1";
// should returns s1
// Impossible should be type-checked as an error since it's not part of the option union type
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3", impossible: "impossible"});

我的问题出现在于泛型类型 T 必须是一个 string 才能用作对象键。我不知道如何让 T 成为 string 的联合类型。
我尝试过 T extends string,但没有成功。

1
T extends string 的问题是什么?它不允许 impossible(我不确定你是否想要这个),但除此之外它可以正常工作:https://www.typescriptlang.org/play/#code/KYDwDg9gTgLgBAYwgOwM7wLYE8DKB3ASxgQAs4BeOAHgBU5QZhkATVOdKA5AcwD4AKAG4BDADYBXYAC44NADRxIqVAQBGBUUQLBUMgN4BtANbAscLrIC6M4ciwBfAJQ27FXnD0BYAFBw-cKGAYcShkRQhlNQ0tHQMRCWBLAG4fexTvHx9mYARRYUC4USC4CDAYAhQZACJ4gEYquAAfOBqxACYG5tbRAGYq9Ox8IlJ+UvKUBT066tR6hXi2mY75sR6ZvoUCDCUVVSLqrZ21IqqnJLggA - Titian Cernicova-Dragomir
我可能会这样做:链接,但我认为我们需要一个 [mcve] 来真正理解你想要做什么。 - jcalz
你的示例使用了一个未初始化的类型。例如,使用“val1”初始化选项。 - Mathieu Urstein
@MathieuUrstein 这是一个不同的问题,因为你用一个值进行初始化,流分析只能证明 val1 是可能的,而不管你的类型注释,所以任何不是 val1 的属性都会出现错误。引入一些不确定性,这个例子就可以工作了: - Titian Cernicova-Dragomir
https://www.typescriptlang.org/play/#code/MYewdgzgLgBAtgTwMoHcCWVgAsYF4YA8AKjAKYAeUpYAJhDNAE5pgDmAfABQBuAhgDYBXUgC4YRADQwADiAgQ0AIzT8MaUhDEBvANoBrUghgtxAXTG8wCAL4BKC1bzsYWgLAAoGF5iNSUQYxgMnIKyqpQ6hA6fEKkpgDcHtaJ7h78fjAg0hHgYgBEMQCMeTAAPjAFAgBMJeWV-ADMJficOvXFUvU1nTFNpjC89KCQULY6ALK8UFgAdIwggrSck9NzljQgcJy2MABUMFW2ph7DECDpM-wgrJyIqBjYnFk5YFJaRfkQHTAxVZ-dPwEDU+TRgdls8SAA - Titian Cernicova-Dragomir
是的,但这正是我尝试解决的问题。我试图将开关简化为一个简单的函数。 - Mathieu Urstein
1个回答

10

T extends string 版本似乎表现良好。它禁止了 impossible,但是您是否想要禁止它呢?因为如果参数永远不能具有该值,那么该选项将是无用的。

export const mySwitch = <T extends string>(value: T, possibilities: {[key in T]: any}): any => {
    return possibilities[value];
};


declare let option: "val1" | "val2" | "val3";
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3", impossible: "impossible"}); 

如果你想允许额外的键,你可以单独声明case对象(绕过多余属性检查并允许重用case对象)。

play


declare let option: "val1" | "val2" | "val3";
const casses = {val1: "s1", val2: "s2", val3: "s3", impossible: "impossible"}
mySwitch(option, casses); 
播放 或者您可以稍微改变类型,使泛型类型参数成为 case object,这样值将被标记为 keyof T
export const mySwitch = <T>(value: keyof T, possibilities: T): any => {
    return possibilities[value];
};


declare let option: "val1" | "val2" | "val3";
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3", impossible: "impossible"}); 
播放

此外,更好的选择是保留案例对象中的类型,而不是使用any

export const mySwitch = <T, K extends keyof T>(value: K, possibilities: T): T[K] => {
    return possibilities[value];
};


declare let option: "val1" | "val2" | "val3";
mySwitch(option, {val1: 1, val2: "s2", val3: "s3", impossible: false});  // returns string | number

编辑:

为保留正确的返回类型并在联合中存在可能性但未出现错误时,可以使用以下代码:

播放

const mySwitch = <T extends Record<K, any>, K extends string>(value: K, possibilities: T & Record<Exclude<keyof T, K>, never>): any => {
    return possibilities[value];
};

let option: "val1" | "val2" | "val3" = (["val1", "val2", "val3"] as const)[Math.round(Math.random() * 2)]
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3" });
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3", impossible: "" }); //err on impossible

请注意,由于TypeScript具有控制流分析功能,因此您需要确保option不仅仅是您指定的类型注释,而是实际分配的常量。

play


感谢您的回复。我已经编辑了我的帖子,以使其更加精确。 我不想让“impossible”键正确,我想让typescript向我显示错误,即“impossible”不是选项联合类型的一部分。 关于您的第一个示例,我希望检查值而不仅仅是类型。 我的例子初始化了选项变量。 - Mathieu Urstein
@MathieuUrstein 我添加了一个新选项,请告诉我你的想法。 - Titian Cernicova-Dragomir
很抱歉,但它实际上是使用 T extends string 工作的,我只是没有意识到我不能尝试使用初始化值。我的错,无论如何,谢谢你让我意识到了这一点。 - Mathieu Urstein
1
@MathieuUrstein 欢迎,我仍然建议您不要使用 any。我的最新版本保留了来自 case 对象的正确类型,这在我看来更好。 - Titian Cernicova-Dragomir
如果我还想防止重复的键怎么办?例如,mySwitch(option, {val1: "s1", val2: "s2", val3: "s2"}) 应该抛出一个错误。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接