【问题标题】:How to force interface to "implement" keys of enum in typescript 3.0?如何在 typescript 3.0 中强制接口“实现”枚举的键?
【发布时间】:2019-01-20 15:03:22
【问题描述】:

假设我有一些枚举 E { A = "a", B = "b"}。 我想强制某些接口或类型(为了便于阅读,我只提到接口)具有 E 的所有键。但是,我想分别指定每个字段的类型。因此,{ [P in E]: any } 甚至{ [P in E]: T } 都不是合适的解决方案。

例如,代码可能包含两个实现 E 的接口:

  • E { A = "a", B = "b"}
  • Interface ISomething { a: string, b: number}
  • Interface ISomethingElse { a: boolean, b: string}

随着 E 在开发过程中的扩展,它可能变成:

  • E { A = "a", B = "b", C="c"}
  • Interface ISomething { a: string, b: number, c: OtherType}
  • Interface ISomethingElse { a: boolean, b: string, c: DiffferntType}

几个小时后:

  • E { A = "a", C="c", D="d"}
  • Interface ISomething { a: string, c: ChosenType, d: CarefullyChosenType}
  • Interface ISomethingElse { a: boolean, c: DiffferntType, d: VeryDifferentType}

等等等等。因此,从https://www.typescriptlang.org/docs/handbook/advanced-types.html 看来,它还不受支持。有没有我遗漏的打字稿技巧?

【问题讨论】:

    标签: typescript types strong-typing


    【解决方案1】:

    我猜你已经致力于写出 enuminterface,然后希望 TypeScript 会警告你 interface 缺少来自 enum 的键(或者如果它有额外的键)?

    假设你有

    enum E { A = "a", B = "b", C="c"};
    interface ISomething { a: string, b: number, c: OtherType};
    

    您可以使用conditional types 让 TypeScript 找出E 的任何成分是否从ISomething 的键中丢失:

    type KeysMissingFromISomething = Exclude<E, keyof ISomething>;
    

    如果ISomething 中没有缺少任何键,则此类型应为never。否则,它将是E 的值之一,如E.C

    您还可以让编译器确定 ISomething 是否有任何 组成 E 的键,也使用条件类型...虽然这更复杂,因为 you can't quite manipulate enums programmatically in expected ways .这里是:

    type ExtraKeysInISomething = { 
      [K in keyof ISomething]: Extract<E, K> extends never ? K : never 
    }[keyof ISomething];
    

    同样,如果您没有额外的密钥,这将是 never。然后,如果其中任何一个不是never,您可以通过使用generic constraintsdefault type parameters 来强制编译时错误:

    type VerifyISomething<
      Missing extends never = KeysMissingFromISomething, 
      Extra extends never = ExtraKeysInISomething
    > = 0;
    

    VerifyISomething 类型本身并不有趣(它始终是0),但是如果它们各自的默认值不是never,泛型参数MissingExtra 会报错。

    让我们试试吧:

    enum E { A = "a", B = "b", C = "c" }
    interface ISomething { a: string, b: number, c: OtherType }
    type VerifyISomething<
      Missing extends never = KeysMissingFromISomething,
      Extra extends never = ExtraKeysInISomething
      > = 0; // no error
    

    enum E { A = "a", B = "b", C = "c" }
    interface ISomething { a: string, b: number } // oops, missing c
    type VerifyISomething<
      Missing extends never = KeysMissingFromISomething, // error!
      Extra extends never = ExtraKeysInISomething
      > = 0; // E.C does not satisfy the constraint
    

    enum E { A = "a", B = "b", C = "c" }
    interface ISomething { a: string, b: number, c: OtherType, d: 1} // oops, extra d
    type VerifyISomething<
      Missing extends never = KeysMissingFromISomething,
      Extra extends never = ExtraKeysInISomething // error!
      > = 0; // type 'd' does not satisfy the constraint
    

    所以所有这些都有效......但它并不漂亮。


    另一种骇人听闻的方法是使用假人class,其唯一目的是在您不添加正确的属性时责骂您:

    enum E { A = "a", B = "b" , C = "c"};
    class CSomething implements Record<E, unknown> {
      a!: string;
      b!: number;
      c!: boolean;
    }
    interface ISomething extends CSomething {}
    

    如果你遗漏了其中一个属性,你会得到一个错误:

    class CSomething implements Record<E, unknown> { // error!
      a!: string;
      b!: number;
    }
    // Class 'CSomething' incorrectly implements interface 'Record<E, unknown>'.
    // Property 'c' is missing in type 'CSomething'.
    

    它不会警告你有额外的属性,尽管你可能不在乎?


    无论如何,希望其中一个对你有用。祝你好运。

    【讨论】:

    • 虚拟类解决方案效果很好 - 谢谢! ISomething 不能直接实现 Record 有什么好的理由吗?似乎可以在编译时简单地检查它。
    • 只有class 定义可以使用implements。所以interface ISomething implements Record&lt;E, unknonwn&gt; { ... } 是一个错误。您可以像在interface ISomething extends Record&lt;E, unknown&gt; {} 中一样使用extends,但它不再警告您缺少属性...ISomething 将已经拥有带有来自E 的键和类型为unknown 的属性。
    【解决方案2】:

    您可以只在枚举上使用映射类型:

    enum E { A = "a", B = "b"}
    
    type AllE = { [P in E]: any }
    
    let o: AllE = {
        [E.A]: 1,
        [E.B]: 2
    };
    
    let o2: AllE = {
        a: 1,
        b :2
    }
    

    Playground link

    编辑

    如果你想让新创建的对象保持原来的属性类型,你会使用一个函数。我们需要该函数来帮助推断新创建的对象字面量的实际类型,同时仍将其限制为具有 E 的所有键

    enum E { A = "a", B = "b" };
    
    
    function createAllE<T extends Record<E, unknown>>(o: T) : T {
        return o
    }
    
    let o = createAllE({
        [E.A]: 1,
        [E.B]: 2
    }); // o is  { [E.A]: number; [E.B]: number; }
    
    let o2 = createAllE({
        a: 1,
        b: 2
    }) // o2 is { a: number; b: number; }
    
    
    let o3 = createAllE({
        a: 2
    }); // error
    

    Playground link

    【讨论】:

    • 不完全是,因为我确实想指定 [E.A] 和 [E.B] 的类型(以及后来的 [E.C] 等类型)。我将编辑问题以澄清它。
    • @poli 编辑了问题以保留类型。如果你想要一个命名接口,如果你没有所有字段,就会责骂你,jcalz answer 可以很好地解决这个问题。
    【解决方案3】:

    使用Record。只需将 Enum 类型放在首位即可。不要使用keyof 或其他任何东西。

    export enum MyEnum {
      FIRST = 'First',
      SECOND = 'Second',
    }
    
    export const DISPLAY_MAP: Record<MyEnum, string> = {
      [MyEnum.FIRST]: 'First Map',
      [MyEnum.SECOND]: 'Second Map',
    }
    

    如果您缺少 TypeScript 会向您大喊大叫的属性之一。

    【讨论】:

      【解决方案4】:

      如果您不想使用字符串枚举,则可以执行以下操作:

      enum E {
        A,
        B,
        C,
      }
      
      type ISomething = Record<keyof typeof E, number>;
      
      const x: ISomething = {
        A: 1,
        B: 2,
        C: 3,
      };
      

      【讨论】:

        【解决方案5】:

        如果您可以使用类型(而不是接口),那么使用内置的Record 类型实际上非常简单:

        enum E { A = "a", B = "b", C="c"}
        type ISomething = Record<E, { a: string, b: number, c: OtherType}>
        type ISomethingElse = Record<E, { a: boolean, b: string, c: DifferentType}>
        

        这样做,如果 ISomethingISomethingElse 省略枚举中的任何键,Typescript 会警告你。

        【讨论】:

        • 我在操场上尝试过这个(版本 3.9.2),如果我在记录定义中遗漏任何值,它似乎不会警告我。
        • 真是个奇怪的人。确实如此。例如。以下代码将无法编译:enum E { A = "a", B = "b", C = "c" }; type ISomething = Record&lt;E, { a: string; b: number }&gt;; const foo: ISomething = { [E.A]: { a: "asdf", b: 123 } };
        【解决方案6】:

        jcalz 上面的优秀答案(即投票最高的答案)在最新版本的 TypeScript 中不起作用,抛出以下错误:

        所有类型参数都未使用。 ts(62305)

        这是因为这两个泛型参数未被使用。我们可以通过简单地使用下划线作为变量名的第一个字符来解决这个问题,如下所示:

        // Make copies of the objects that we need to verify so that we can easily
        // re-use the code block below
        type EnumToCheck = MyEnum;
        type InterfaceToCheck = MyInterface;
        
        // Throw a compiler error if InterfaceToCheck does not match the values of EnumToCheck
        // From: https://stackoverflow.com/questions/51829842
        type KeysMissing = Exclude<EnumToCheck, keyof InterfaceToCheck>;
        type ExtraKeys = {
          [K in keyof InterfaceToCheck]: Extract<EnumToCheck, K> extends never
            ? K
            : never;
        }[keyof InterfaceToCheck];
        type Verify<
          _Missing extends never = KeysMissing,
          _Extra extends never = ExtraKeys,
        > = 0;
        

        请注意,如果您使用 ESLint,您可能需要在某些行上添加 eslint-disable-line 注释,因为:

        1. “验证”类型未使用
        2. 两个泛型参数未使用

        【讨论】:

          猜你喜欢
          • 2019-06-03
          • 1970-01-01
          • 1970-01-01
          • 2016-09-03
          • 2020-08-31
          • 2023-04-03
          • 1970-01-01
          • 2019-10-18
          • 1970-01-01
          相关资源
          最近更新 更多