typescript - 使用混合时,将参数约束为文字值的某个有限联合
问题描述
我需要一种方法将泛型函数的参数限制为文字值的联合,即
'foo'
,'foo' | 'bar'
,42
, 等必须被允许string
或number
不得string & SomethingElse
或者number & AnotherThing
也不能('foo' | 'bar') & SomethingElse
,42 & AnotherThing
应该被允许(不太重要)
考虑这样的函数
declare function test<K extends string>(t: { [P in K]: string; }): void;
这个想法是我们希望每个可能的值K
都有一个对应的属性 in t
,这样我们就可以t
根据一些K
类型的值来查找 in 的值。
一些显示一切顺利的示例用法:
declare const foobar: { foo: 'bar'; };
test(foobar); // compiles, correct
test<'foo'>(foobar); // compiles, correct
test<'bar'>(foobar); // compiler error, correct
// ^^^^^^
// Argument of type '{ foo: "bar"; }' is not assignable to parameter of type '{ bar: string; }'.
// Property 'bar' is missing in type '{ foo: "bar"; }' but required in type '{ bar: string; }'.
但是,有一个问题:
test<string>(foobar); // compiles, INCORRECT
在这种test<string>
情况下,{ [P in K]: string; }
become { [P: string]: string; }
,它不能保证每个值实际上都有一个键(事实上,这是不可能的,除非有一些代理 getter 恶作剧)。因此,我不想{ [P: string]: string; }
在我的函数中允许参数test
,这相当于说我不希望我的test
函数允许string
作为K
.
现在,人们可以绕过这种方式。考虑
declare function test<K extends string>(t: string extends K ? never : { [P in K]: string; }): void;
测试string extends K
告诉我们是否string
已传递给K
,在这种情况下,我们会设置参数never
,以防止将任何实际值传递给函数(最好在此处引发错误,但 Typescript 不支持)。所以
test<string>(foobar); // compiler error, correct
// ^^^^^^
// Argument of type '{ foo: "bar"; }' is not assignable to parameter of type 'never'.
但是,当我们使用字符串混合时,这并不能解决问题,我们的项目就是这样做的。考虑
type StringExtended = string & { extension(): void; };
test<StringExtended>(foobar); // compiles, INCORRECT
因为string extends StringExtended
是假的,我们的never
hack 没有被应用,我们回到了{ [P in K]: string; }
,奇怪的是现在推断为{}
而不是{ [P: string]: string; }
。但是,无论哪种方式,它仍然可以编译。我不希望这样,因为我们的项目使用了很多这样的扩展(特别是使用品牌原语K extends string
),而且对于一些旨在让字符串文字代替接受这些类型的字符串扩展的约束来说,这太合理了,打破函数的类型安全。
对于一个更“现实”的例子,考虑
interface Keyed<K extends string, D> { key: K; data: D; }
function toNameOf<K extends string>(
{ key }: Keyed<K, any>,
names: string extends K ? never : { [P in K]: string; },
): string {
return names[key];
}
declare const k1: Keyed<'foo' | 'bar', number>;
toNameOf(k1, { foo: 'Foo', bar: 'Bar' });
toNameOf(k1, {});
// ^^
toNameOf(k1, { foo: 'Foo' });
// ^^^^^^^^^^^^^^
toNameOf(k1, { bar: 'Bar' });
// ^^^^^^^^^^^^^^
declare const k2: Keyed<string, number>;
toNameOf(k2, { foo: 'Foo', bar: 'Bar' });
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
toNameOf(k2, {});
// ^^
toNameOf(k2, { foo: 'Foo' });
// ^^^^^^^^^^^^^^
toNameOf(k2, { bar: 'Bar' });
// ^^^^^^^^^^^^^^
declare const k3: Keyed<StringExtended, number>;
toNameOf(k3, { foo: 'Foo', bar: 'Bar' });
toNameOf(k3, {});
toNameOf(k3, { foo: 'Foo' });
toNameOf(k3, { bar: 'Bar' });
k1
除非我们得到一个names
实际上涵盖所有可能性的论点,否则这些案例正确地失败了。在 中k2
,我们只是拒绝一切;k2
没有toNamesOf
支持的类型。但是,尽管我尽了最大努力,但在这些k3
情况下,所有编译都没有错误或警告。类似的结构在我们的代码中随处可见Keyed
(正确地,在其他用例中)用StringExtended
而不是任何特定的文字填充。
解决方案
我找到了一个我认为足够强大的解决方案。它没有涵盖第四个要点,但如前所述,这对我来说不太重要。
type LikeLiteralUnion<K extends string | number | symbol> =
string extends K ? never :
number extends K ? never :
symbol extends K ? never :
[{}] extends [{ [P in K]: unknown; }] ? never :
K;
我在问题中指出,当您使用 mixin 时,例如type K = string & { extension(): void; }
,{ [P in K]: unknown; }
推断为{}
而不是{ [P: string]: unknown; }
. 事实证明,这是我们可以利用的两者之间的区别。
使用喜欢
declare function takeLiteralUnion<K extends string | number | symbol>(k: LikeLiteralUnion<K>): void;
推荐阅读
- ajax - Drupal 7 Ajax 与 tableselect 和字段后续更新
- javascript - Javascript / React window.onerror 触发了两次
- javascript - 如何从单击事件处理程序中获取 ID?
- node.js - UnhandledPromiseRejectionWarning:错误:无法存储合约代码,请检查您的gas限制
- php - Linux Screen 实用程序在重定向后不回显到屏幕
- android - 导航抽屉问题(不显示布局预览)
- node.js - 在电子中使用 node.js 模块
- semantic-ui - .babelrc 破坏语义用户界面和下一个应用程序
- machine-learning - Keras 不切实际的结果
- javascript - jQuery caoursel 滑块不更新窗口调整大小的宽度