打字稿对象字面量缩小

Typescript object literal narrowing

我在谷歌上搜索了将近两个小时,所以现在我转向 Stack Overflow 寻求帮助。我正在编写一个接受对象参数的函数。我不知道如何在传递对象文字时强制 TS 缩小到类型。我的简单工作示例看起来像下面的代码,但感觉不必要地麻烦:

type ValueOf<T> = T[keyof T];

const getRandomValue = <T, S=Record<string, T>>(obj:S):T => {
    const values = Object.values(obj);
    const randomIndex = Math.floor(Math.random() * values.length);

    return values[randomIndex];
}

const obj = {
    a: "hi",
    b: "hello",
} as const;

//this works as expected but it's cumbersome to define the literal separately and have to say "as const" to avoid the type becoming { string: string }
const value = getRandomValue<ValueOf<typeof obj>>(obj);

//prove that it works
type ValueType = typeof value;
const x:ValueType = "hello"; //allowed
const y:ValueType = "bonjour"; //shows a type error

//I was hoping I could define the function in a way where I could call it like this and still
//get the same strict typing as above: getRandomValue({ a: "hi", b: "hello" });

playground link

编辑:删除了代码的非工作版本。

当您使用 T = Record<string, string> 时,您的 ValueOf<T> 会发生什么情况是 TS(正确地)确定 Record<string, string>[string] = string.

当您将 getRandomValue<T>(obj: T): ValueOf<T> 与对象文字一起使用时,例如 { foo: "bar" }T 不是 Record<string, string> 而是 Record<"foo", "bar">.

因此您需要对 记录的 键和值类型使用泛型:

const getRandomValueFromObj = <Key, Value>(obj: Record<Key, Value>): Value => {}

但是,出于几个不同的原因,这还不够。首先,TS 仍会确定 Key = stringValue = string,例如{ foo: "bar" },但你想要字符串常量。你可以写 { foo: "bar" as const } 来向 TS 保证 "bar" 是常量,但是在函数定义中使用 generic constraint 更容易:

const getRandomValueFromObj =
    <Key, Value extends string>(obj: Record<Key, Value>): Value => {}

现在打字稿将为 { foo: "bar" } 确定 Value = "bar"

接下来,您将遇到 KeyObject.keys 的问题,它们的签名是 .keys(o: {}): string[]。也就是说,即使 oRecord<Key, Value>Object.keys 也总是 return string[],而 that's intentional。您必须使用类型断言:

const getRandomValueFromObj =
    <Key, Value extends string>(obj: Record<Key, Value>): Value => {
        const keys = Object.keys(obj) as Key[];
    }

但 TS 仍然不高兴,因为 Keystring 可能不重叠。毕竟,Key 可以是任何东西!您还必须限制 Key

const getRandomValueFromObj =
    <K extends string, V extends string>(obj: Record<K, V>): V => {
        const keys = Object.keys(obj) as Key[];
    }

这是一个可以使用的游乐场 link