typescript - 对象的类型定义,它对类型的值进行(自我)引用
问题描述
是否可以为对象定义一种类型,其中一个键类型引用另一个键的值?
假设我们想要一个可以定义Configuration
的类型。languages
如何确保密钥defaultLanguage
是其中一种语言?languages
应该充当联合类型。
可以写..
export const languages = ['de-CH', 'en-GB'] as const;
type Language = typeof languages[number];
export const defaultLanguage: Language = 'de-CH';
..,但是否也可以为一个对象编写类型,它表示相同而不提取languages
?
type Configuration = {
languages: string[],
defaultLanguage: // A type which takes the values of languages and uses them as an union type here
}
这里有两个简单的测试用例:
const goodConfig: Configuration = {
languages: ['de-CH', 'en-GB'],
defaultLanguage: 'de-CH'
}
const badConfig: Configuration = {
languages: ['de-CH', 'en-GB'],
defaultLanguage: 'de-DE' // <- type error
}
解决方案
除非您想从中选择子集的可能语言字符串文字类型的一些中等大小的联合,否则您不能在 TypeScript 中将其表示Configuration
为特定类型。如果该属性真的可以接受任何可能字符串的任何数组,那么特定类型将必须是所有可能性的无限联合,这在 TypeScript 中是无法表达的。languages
Configuration
--
相反,您可以表示Configuration<L>
为泛型类型,如下所示:
type Configuration<L extends string> = {
languages: L[],
defaultLanguage: L;
}
这个想法是,L
这里表示特定配置对象的可能语言字符串文字的联合。该languages
属性是这些的数组,而该defaultLanguage
属性只是其中之一。
从概念上讲,您想说“aConfiguration
是我不知道或不关心的某种类型的Configuration<L>
对象”。这种只关心参数是某种类型的泛型类型称为存在类型。不幸的是,TypeScript 不直接支持存在类型(尽管它们已被请求,请参阅microsoft/TypeScript#14466)。所以现在没有简单的方法可以说。L
type DesiredConfiguration = <∃L>Configuration L
这意味着为了使用Configuration<L>
,L
必须以某种方式指定类型。它可以通过注释手动完成,但这很烦人:
const annoyingConfig: Configuration<'de-CH' | 'en-GB'> = {
languages: ['de-CH', 'en-GB'],
defaultLanguage: 'de-CH'
}
布莱奇。相反,我们可以尝试让编译器推断出L
何时面临 type 的候选值Configuration<L>
。为此,我们引入了一个通用的“无所事事”或“身份”函数,它只在运行时返回其输入,但通用函数调用可用于使编译器推断L
:
const asConfiguration = <L extends string>(config: Configuration<L>) => config;
const goodConfig = asConfiguration({
languages: ['de-CH', 'en-GB'],
defaultLanguage: 'de-CH'
})
// const goodConfig: Configuration<"de-CH" | "en-GB">
万岁,推理奏效了。现在让我们看看错误的配置错误:
const badConfig = asConfiguration({
languages: ['de-CH', 'en-GB'],
defaultLanguage: 'de-DE' // wait, no error?
});
// const badConfig: Configuration<"de-CH" | "en-GB" | "de-DE">
哦,没有错误。推理效果太好了,现在L
作为所有三个值的联合!
理想情况下,您想说“在推断 a 时,仅Configuration<L>
使用languages
属性,并且仅检查defaultLanguage
属性。我们希望L
indefaultLanguage
是“非推断性的”。这是功能请求的主题,microsoft/TypeScript# 14829 . 这个想法是我们将Configuration<L>
' 定义更改为:
type Configuration<L extends string> = {
languages: L[],
defaultLanguage: NoInfer<L>;
}
的一些合适的定义NoInfer
。没有正式版本NoInfer
,而且 GitHub 问题仍然开放。但是那里的讨论确实为可能的自己动手实现提供了一些建议,例如这个建议:
type NoInfer<T> = T & {}
或这个建议:
type NoInfer<T> = [T][T extends any ? 0 : 1];
我通常选择后者。如果我们使用它,现在事情就会如愿以偿。goodConfig
仍然很好,现在很badConfig
糟糕:
const badConfig = asConfiguration({
languages: ['de-CH', 'en-GB'],
defaultLanguage: 'de-DE' // error!
// Type '"de-DE"' is not assignable to type '"de-CH" | "en-GB"'
});
// const badConfig: Configuration<"de-CH" | "en-GB">
这现在有效。使用泛型类型而不是特定类型的一个缺点是,现在您想要依赖的代码中的任何内容现在Configuration
也必须是泛型的。与其将这个L
参数分布在你的代码库中,你可能希望只保留它以便它验证开发人员指定的配置对象,但扩大到你自己的代码的不受约束的版本。
因此,面向外部的代码将验证,然后调用不关心该约束的面向内部的代码:
function externalFacingCode<L extends string>(c: Configuration<L>) {
internalFacingCode(c);
}
interface InternalConfiguration {
languages: string[],
defaultLanguage: string
}
function internalFacingCode(c: InternalConfiguration) {
c.languages.find(x => x === c.defaultLanguage)!.toUpperCase();
}
回顾:
- 您目前无法表示
Configuration
为特定类型。 - 您可以将其表示为泛型类型,但您需要指定其类型参数。
- 通过注释手动执行此操作很乏味。
- 通过辅助函数更容易做到,但设置函数很棘手。
- 无论如何,它比特定类型更复杂,因此您可能希望将其使用限制为仅验证代码。
推荐阅读
- raspberry-pi - 我可以创建自己的 Raspberry Pi OS 并将其放入 GitHub 吗?
- fix-protocol - 如何跨多个部分填充报告佣金(标签 12)
- powerbi - 在 Powerquery 中根据当前月份创建具有固定值的列
- c# - 在 Xamarin.Forms 中使用 RadioButton
- java - 弹簧数据规范匹配/过滤器转换数字列
- javascript - 烧瓶 send_file 到 Javascript 文件对象
- azure - 需要升级天蓝色服务器
- json.net - JSON.NET DefaultNamingStrategy 不适用于 IDictionary
- c# - WPF 根据使用 DataTemplate 的 ComboBox 选择更改其他控件属性
- delphi - 它是为 Delphi ISAPI DLL 中的每个请求创建的 TWebModule