TypeScript:为只有一个键的对象键入(不允许联合类型作为键)

Max*_*ber 5 typescript union-types

我希望定义一个type可以只有一个键的对象。

这是一个尝试:

type OneKey<K extends string> = Record<K, any>
Run Code Online (Sandbox Code Playgroud)

不幸的是,这并不完全有效,因为变量可以具有联合类型:

type OneKey<K extends string> = Record<K, any>

declare function create<
    K extends string,
    T extends OneKey<K>[K]
>(s: K): OneKey<K>

const a = "a";
const res = create(a);


// Good
const check: typeof res = { a: 1, b: 2 }
//                                ~~ Error, object may only specify known properties

declare const many: "a" | "b";
const res2 = create(many);


// **Bad**: I only want one key
const check2: typeof res2 = { a: 1, b: 2 }; // No error
declare const x: "k1" | "k2"
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 8

如果我理解正确,你想OneKey<"a" | "b">成为像{a: any, b?: never} | {a?: never, b: any}. 这意味着它要么有一个akey要么有一个bkey但不是两者都有。所以你希望类型是某种联合来表示它的要么-要么部分。此外,联合类型{a: any} | {b: any}的限制性不够,因为 TypeScript 中的类型是开放/可扩展的,并且总是可以具有未知的额外属性......意味着类型不是精确的。因此该值{a: 1, b: 2}确实与 type 匹配{a: any},并且目前在 TypeScript 中不支持具体表示诸如Exact<{a: any}>允许{a: 1}但禁止之类的内容{a: 1, b: 2}

话虽如此,TypeScript 确实有多余的属性检查,其中对象文字被视为具有精确类型。在这种check情况下这对您有用(错误“对象文字可能仅指定已知属性”是过度属性检查的具体结果)。但在check2情况下,相关类型将成为工会一样{a: any} | {b: any}......既然都ab都存在于联盟中的至少一个成员,多余的属性检验不会在那里踢,至少TS3.5的。这被认为是一个错误;大概{a: 1, b: 2}应该没有通过多余的财产检查,因为它对联合的每个成员都有多余的财产。但尚不清楚何时甚至是否会解决该错误。

在任何情况下,最好对OneKey<"a" | "b">一个类型进行评估,例如{a: any, b?: never} | {a?: never, b: any}... 该类型{a: any, b?: never}将匹配,{a: 1}因为b是可选的,但不是{a: 1, b: 2},因为2不可分配给never。这将为您提供您想要的但不是两者兼而有之的行为。

在我们开始编写代码之前的最后一件事: type{k?: never}等价于 type {k?: undefined},因为可选属性总是可以有一个undefined值(并且 TypeScript 在区分缺失undefined和 的方面做得不是很好)。

这是我可能的做法:

type OneKey<K extends string, V = any> = {
  [P in K]: (Record<P, V> &
    Partial<Record<Exclude<K, P>, never>>) extends infer O
    ? { [Q in keyof O]: O[Q] }
    : never
}[K];
Run Code Online (Sandbox Code Playgroud)

我允许V使用某些值类型,any除非您想专门使用它number或其他东西,但它会默认为any. 它的工作方式是使用一个映射类型在每个值迭代PK,并为每个值的属性。这个属性本质上是Record<P, V>(所以它确实有一个P键)与Partial<Record<Exclude<K, P>, never>>...相交...Exclude从联合中删除成员,所以Record<Exclude<K, P>, never>是一个对象类型,每个键都在K except 中 P,其属性是never。并且Partial使密钥可选。

类型Record<P, V> & Partial<Record<Exclude<K, P>, never>>很难看,所以我使用条件类型推断技巧使它再次变得漂亮......T extends infer U ? {[K in keyof U]: U[K]} : never将采用一个 type T,将它“复制”到一个 type U,然后显式迭代它的属性。它将采用 like 类型{x: string} & {y: number}并将其折叠为{x: string; y: number}.

最后,映射类型{[P in K]: ...}本身不是我们想要的;我们需要它的值类型为联合,所以我们查找通过这些值{[P in K]: ...}[K]

请注意,您的create()函数应该像这样输入:

declare function create<K extends string>(s: K): OneKey<K>;
Run Code Online (Sandbox Code Playgroud)

没有T它。让我们测试一下:

const a = "a";
const res = create(a);
// const res: { a: any; }
Run Code Online (Sandbox Code Playgroud)

所以res仍然{a: any}是您想要的类型,并且行为相同:

// Good
const check: typeof res = { a: 1, b: 2 };
//                                ~~ Error, object may only specify known properties
Run Code Online (Sandbox Code Playgroud)

不过,现在我们有了:

declare const many: "a" | "b";
const res2 = create(many);
// const res2: { a: any; b?: undefined; } | { b: any; a?: undefined; }
Run Code Online (Sandbox Code Playgroud)

这就是我们想要的工会。它能解决你的check2问题吗?

const check2: typeof res2 = { a: 1, b: 2 }; // error, as desired
//    ~~~~~~ <-- Type 'number' is not assignable to type 'undefined'.
Run Code Online (Sandbox Code Playgroud)

是的!


需要考虑的一个警告:如果 to 的参数create()只是 astring而不是字符串文字的联合,则结果类型将具有字符串索引签名并且可以采用任意数量的键:

declare const s: string
const beware = create(s) // {[k: string]: any}
const b: typeof beware = {a: 1, b: 2, c: 3}; // no error
Run Code Online (Sandbox Code Playgroud)

无法跨 分布string,因此无法在 TypeScript 中表示类型“具有来自所有可能字符串文字集合的单个键的对象类型”。您可能会更改为禁止 type 参数,但是这个答案已经足够长了。如果您足够关心尝试处理它,这取决于您。create()string


好的,希望有帮助;祝你好运!

代码链接

  • 感谢您的回复。我想我正在寻找的是禁止工会。我显然是一只资本主义猪。 (10认同)