【问题标题】:Specifying type of value without specifying type of key for object指定值的类型而不指定对象的键类型
【发布时间】:2020-11-18 12:48:59
【问题描述】:

我有一个相当复杂的interface,它将有许多众所周知的实例。这些实例应该可以通过某种 ID 引用。理想情况下,我想在一个对象中定义所有这些众所周知的实例,以便我可以使用keyof 运算符轻松地为键设置一个类型。

interface Complex { 
  frobs: number[];
  type: 'A' | 'B';
} // More complicated in reality

const COMMON = {
  foo: { type: 'A', frobs: [] },           // this is fine
  bar: { type: 'B', frobs: [1], zorg: 1 }, // unwanted extra
  baz: { type: 'A', noFrobs: "uh-oh" },    // frobs missing
}

Type CommonId = keyof typeof COMMON; // Great, valid Ids are "foo" | "bar" | "baz"

但是如果我指定没有任何类型的对象(如上所述),Typescript 当然不会强制该对象中的所有值都应该是相同的类型。

所以我想指定所有值都必须是Complex 类型,但如果不指定键类型,我就无法这样做。现在我必须两次提到每个键:一次是 CommonId 的一部分,一次是对象:

interface Complex { 
  frobs: number[]; 
  type: 'A' | 'B';
} // More complicated in reality
type CommonId = "foo" | "bar";         // Much more common objects in reality

const COMMON: { 
  [commonId in CommonId]: Complex 
} = {
  foo: { type: 'A', frobs: [] },
  bar: { type: 'B', frobs: [1] }
}

有没有办法两全其美?我很想用一个新的键值对扩展 COMMON 变量,以自动将该键添加到 CommonId 类型。

【问题讨论】:

    标签: typescript


    【解决方案1】:

    您可以先创建一个不输入的变量,使用它来获取键类型,然后导出相同的值但使用类型:

    interface Complex { frobs: number[]; }
    
    const _common = {
      foo: { frobs: [] },
      bar: { frobs: [1] }
    };
    
    type CommonId = keyof typeof _common;
    
    export const COMMON: { 
      [commonId in CommonId]: Complex 
    } = _common;
    

    Playground link

    【讨论】:

    • 当,我选择了一个太简单的Complex类型。您的示例几乎可以正常工作,但是由于我将联合类型作为复杂对象的一部分,因此我收到了诸如 type string is not assignable to 'A' | 'B' 之类的错误
    • Complex 类型如果您将其用作值则无关紧要。 (see here) 我怀疑由于您的键类型现在受到限制,当您尝试使用string 类型索引对象时,TS 会因为不是键类型之一而抱怨。
    • 这是 Typescript Playground 上代码的链接,遗憾的是它无法编译:typescriptlang.org/play?#code/…
    • 啊,错误似乎来自您的联合依赖于值类型这一事实,打字稿不会自动将值缩小到。
    【解决方案2】:

    这是一个进一步简化的解决方案(大部分功劳归功于 Aplet123,因为它受到他的回答,尤其是他的 cmets 的高度启发,我只写了一个额外的通用函数)。我的用例实际上是定义一个配置,因此是 makeConfig 名称。

    export function makeConfig<C>() {
      return <K extends string>(cfg: Record<K, C>) => cfg;
    }
    

    如果有用,您可以导出并重新使用通用类型的配置函数:

    interface Complex {
      frobs: number[];
      type: 'A' | 'B';
    }
    
    export const complexConfig = makeConfig<Complex>();
    

    然后像这样简单地使用它:

    export const COMMON = complexConfig({
      foo: { type: 'A', frobs: [] },
      bar: { type: 'B', frobs: [1] }
    });
    

    (请注意,不需要像 CommonId 这样的其他变量/类型声明,除非您想导出它们)

    或者,如果您只想对复杂类型使用一次:

    export const COMMON = makeConfig<Complex>()({
      foo: { type: 'A', frobs: [] },
      bar: { type: 'B', frobs: [1] }
    });
    
    

    编辑:或更简洁:

    // generic definition
    type ValueTypeCheck<C> = <K extends string>(x: Record<K,C>) => Record<K,C>;
    
    const valueType = <C,>() => (x => x) as ValueTypeCheck<C>;
    
    // usage
    const complex = valueType<Complex>();
    
    export const COMMON = complex({...});
    

    总的来说,我们确实需要 TS 中的内置机制来强制值的类型,而不使用索引类型,这样会丢失有关键名的信息...

    【讨论】:

      猜你喜欢
      • 2020-08-03
      • 2022-10-12
      • 1970-01-01
      • 1970-01-01
      • 2015-11-06
      • 1970-01-01
      • 1970-01-01
      • 2016-08-17
      • 1970-01-01
      相关资源
      最近更新 更多