【问题标题】:Extend a `Record<string, string[]>` with a different type of property使用不同类型的属性扩展 `Record<string, string[]>`
【发布时间】:2019-12-04 02:34:51
【问题描述】:

在 Typescript 中,以下似乎应该完成所需类型的创建:

interface RecordX extends Record<string, string[]> {
  id: string
}

但这抱怨:

“string”类型的属性“id”不能分配给字符串索引类型“string[]”。 ts(2411)

如何将不同类型的属性添加到Record&lt;&gt; 实用程序类型?

细节和一般案例+样本

一般来说,如何描述具有异质值类型固定属性但动态添加的同质值类型属性的对象。

S,例如给定这个对象:

const a = {
   // some properties with known "hard-coded" types
   id: '123',
   count: 123,
   set: new Set<number>(),

   // and some dynamic properties
   dynamicItemList: ['X', 'Y']
   anotherDynamicallyAddedList: ['Y', 'Z']
} as ExtensibleRecord

那么如何定义一个类型或接口ExtensibleRecord where:

  1. idcountset的类型和属性键固定为stringnumberSet&lt;number&gt;
  2. dynamicItemListanotherDynamicallyAddedList 的类型以及添加到对象的任何其他属性为string[]

我尝试了许多我认为可能可行的变体,包括:

type ExtensibleRecord = {
  id: string, count: number, set: Set<number>
} & Record<string, string[]>

type ExtensibleRecord = {
  id: string, count: number, set: Set<Number>
} & Omit<Record<string, string[]>, 'id'|'count'|'set'>

interface ExtensibleRecord = {
  id: string,
  count: number,
  set: Set<number>,
  [k: string]: string[]
}

但每个似乎都会导致错误。

这感觉很常见且显而易见,但我找不到示例或参考。

playground

【问题讨论】:

  • Record&lt;string, string[]&gt; 表示id 属性(如果存在)必须是string[]。你不能用不匹配的东西来扩展它;你试图做一个例外,而不是一个扩展,并且 TypeScript 不(还)支持它作为一个具体的类型。如果您想获得关于如何操作的建议,您可能希望扩展您的用例。请确保您的代码是minimal reproducible example;现在,与 Record 的名称冲突正在掩盖您的问题。
  • @jcalz 添加了更多描述 + MWE
  • 原因是 TypeScript 试图在对象字面量方面做得更多。这样做是为了避免明显的错误。因此,如果在对象字面量构造之后添加动态属性,它将起作用。

标签: typescript


【解决方案1】:

Pedro 的回答是 100% 正确的,是我旅程的开始,谢谢!

我想为此找到最简单的解决方法,并找到了一个解决方案:Object.assign()。手动构造这些对象的类型错误似乎除外,甚至在使用扩展运算符时,可能是作为与原生 JavaScript 功能的 TypeScript 互操作。

Object.assign()包装在工厂函数中并结合并行类型对我来说感觉很优雅,也许对其他人有用。

// FACTORY WITH TYPE

// Factory uses Object.assign(), which does not error, and returns intersection type
function RecordX(
  fixedProps: { id: string },
  dynamicProps?: Record<string, string[]>
) {
  return Object.assign({}, dynamicProps, fixedProps);
}

// Type name conveniently overlaps with factory and resolves to:
// 
// type RecordX = Record<string, string[]> & {
//   id: string;
// }
type RecordX = ReturnType<typeof RecordX>;

// USAGE

// Standard use
const model: RecordX = RecordX({ id: 'id-1' }, {
  foo: ['a'],
});

// Correct type: string
const id = model.id;

// Correct type: string[]
const otherProp = model.otherProp;

// Appropriately errors with "string is not assignable to string[]"
const model2: RecordX = RecordX({ id: 'id-2' }, {
  bar: 'b'
});

【讨论】:

    【解决方案2】:

    来自官方docs,无法实现你想要的:

    虽然字符串索引签名是描述 “字典”模式,它们还强制所有属性匹配 他们的返回类型。这是因为字符串索引声明 obj.property 也可用作 obj["property"]。在下面的 例如,名称的类型与字符串索引的类型不匹配,并且 类型检查器给出错误:

    interface NumberDictionary {
        [index: string]: number;
        length: number;    // ok, length is a number
        name: string;      // error, the type of 'name' is not a subtype of the indexer
    }
    

    但是,根据这个site,在一种情况下可以从索引签名中排除某些属性:当您想要对声明的变量进行建模时。我正在从site 复制整个部分。

    有时您需要将属性组合到索引签名中。 不建议这样做,您应该使用嵌套索引签名 上面提到的模式。但是,如果您正在建模现有 JavaScript 你可以用交叉类型绕过它。这 以下显示了您将遇到的错误示例 使用交叉点:

    type FieldState = {
      value: string
    }
    
    type FormState = {
      isValid: boolean  // Error: Does not conform to the index signature
      [fieldName: string]: FieldState
    }
    

    这是使用交叉点类型的解决方法:

    type FieldState = {
      value: string
    }
    
    type FormState =
      { isValid: boolean }
      & { [fieldName: string]: FieldState }
    

    请注意,即使您可以将其声明为对现有 JavaScript 建模, 你不能使用 TypeScript 创建这样的对象:

    type FieldState = {
      value: string
    }
    
    type FormState =
      { isValid: boolean }
      & { [fieldName: string]: FieldState }
    
    
    // Use it for some JavaScript object you are gettting from somewhere 
    declare const foo:FormState; 
    
    const isValidBool = foo.isValid;
    const somethingFieldState = foo['something'];
    
    // Using it to create a TypeScript object will not work
    const bar: FormState = { // Error `isValid` not assignable to `FieldState
      isValid: false
    }
    

    最后,作为一种解决方法,如果合适,您可以创建一个嵌套接口(查看site 的“设计模式:嵌套索引签名”部分,其中动态字段位于接口的属性中。对于实例

    interface RecordX {
      id: string,
      count: number,
      set: Set<number>,
      dynamicFields: {
        [k: string]: string[]
      }
    }
    

    【讨论】:

      【解决方案3】:

      如果你不是将它用于class,你可以使用type来描述它:

      type RecordWithID = Record<string, string[]> & {
        id: string
      }
      
      let x: RecordWithID = {} as any
      
      x.id = 'abc'
      
      x.abc = ['some-string']
      

      http://www.typescriptlang.org/play/#code/C4TwDgpgBAShDGB7ATgEwOoEtgAsCSAIlALywIqoA8AzsMpgHYDmANFLfcwNoC6AfFABkUAN4BYAFBQomVAC52dRk0kBfSZIA2EYFAAeCuEjRZchEqNVQAhtRsMQGiXoB0siwHJrAI3genrj7wFlxe1KgAZh48TkA

      【讨论】:

      • — 我遇到了一些问题,所以我更新了这个问题的更多细节,并(希望暂时)取消标记为正确的解决方案。
      猜你喜欢
      • 1970-01-01
      • 2020-08-20
      • 2022-09-23
      • 1970-01-01
      • 1970-01-01
      • 2018-03-03
      • 2021-12-04
      • 2023-04-03
      • 1970-01-01
      相关资源
      最近更新 更多