首页 > 解决方案 > 对象的类型定义,它对类型的值进行(自我)引用

问题描述

是否可以为对象定义一种类型,其中一个键类型引用另一个键的值?

假设我们想要一个可以定义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

解决方案


除非您想从中选择子集的可能语言字符串文字类型的一些中等大小的联合,否则您不能在 TypeScript 中将其表示Configuration特定类型。如果该属性真的可以接受任何可能字符串的任何数组,那么特定类型将必须是所有可能性的无限联合,这在 TypeScript 中是无法表达的。languagesConfiguration

--

相反,您可以表示Configuration<L>泛型类型,如下所示:

type Configuration<L extends string> = {
    languages: L[],
    defaultLanguage: L;
}

这个想法是,L这里表示特定配置对象的可能语言字符串文字的联合。该languages属性是这些的数组,而该defaultLanguage属性只是其中之一。

从概念上讲,您想说“aConfiguration是我不知道或不关心的某种类型的Configuration<L>对象”。这种只关心参数是某种类型的泛型类型称为存在类型。不幸的是,TypeScript 不直接支持存在类型(尽管它们已被请求,请参阅microsoft/TypeScript#14466)。所以现在没有简单的方法可以说。Ltype 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属性。我们希望LindefaultLanguage是“非推断性的”。这是功能请求的主题,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为特定类型。
  • 可以将其表示为泛型类型,但您需要指定其类型参数。
  • 通过注释手动执行此操作很乏味。
  • 通过辅助函数更容易做到,但设置函数很棘手。
  • 无论如何,它比特定类型更复杂,因此您可能希望将其使用限制为仅验证代码。

Playground 代码链接


推荐阅读