首页 > 解决方案 > Typescript 接口属性作为通用对象的嵌套键

问题描述

基于以下结构:

interface ApiEntity {
  [key: string]: number | string;
}

interface ApiData {
  [key: string]: ApiEntity;
}

interface Entity1 extends ApiEntity {
  id: number;
  name: string;
}

interface Entity2 extends ApiEntity {
  uuid: string;
  description: string;
}

interface MyData extends ApiData {
  a: Entity1;
  b: Entity2;
}

如何创建一个只接受有效实体和属性的接口:

// The problem
interface DataFields<T extends ApiData> {
  label: string;
  entity: keyof T; // ensure that entity is one of the properites of abstract ApiData
  property: keyof keyof T; // ensure that property is one of the properties of ApiEntity
  other?: string;
}

因此创建的字段是安全的,并且 TS 在无效时显示错误:

const fields: MyDataFields<MyData>[] = [{
  label: 'A ID',
  entity: 'a', // valid
  property: 'id', // valid
},{
  label: 'B Description',
  entity: 'b', // valid
  property: 'description', // valid
},{
  label: 'Invalid',
  entity: 'c', // TS Error
  property: 'name', // TS Error
}];

甚至更好:

const MyDataFields: DataField<MyData>[] = [
  {label: 'A ID', entityProperty: 'a.id'},
  {label: 'B Description', entityProperty: 'b.description'},
  {label: 'Invalid', entityProperty: 'c.name'}, // TS Error
];

标签: typescripttypescript-generics

解决方案


使用您定义的接口层次结构,其中ApiDataApiEntity声明允许任何字符串作为属性名称,Typescript 根本无法推断这c不是有效的属性名称,MyData或者name不是有效的属性名称Entity2。相反,Typescript 会根据接口的声明方式推断出这些有效的属性名称:

function foo(obj: Entity1): void {
  // no type error
  console.log(obj.foo);
}
function bar(obj: MyData): void {
  // no type error
  console.log(obj.bar);
}

但是,如果您摆脱ApiDataandApiEntity或至少缩小它们以不允许所有字符串作为属性名称,则可以解决此问题。

的有效值property取决于 的值entity,因此这需要是可区分的联合类型,其中entity是判别式。我们可以使用映射类型来构造它:

interface Entity1 {
  id: number;
  name: string;
}

interface Entity2 {
  uuid: string;
  description: string;
}

interface MyData {
  a: Entity1;
  b: Entity2;
}

type DataFields<T> = {
  [K in keyof T]: {
    label: string,
    entity: K,
    property: keyof (T[K])
  }
}[keyof T]

例子:

const fields: DataFields<MyData>[] = [{
  label: 'A ID',
  entity: 'a', // OK
  property: 'id', // OK
}, {
  label: 'B Description',
  entity: 'b', // OK
  property: 'description', // OK
}, {
  label: 'Invalid',
  // Type error: 'c' is not assignable to 'a' | 'b'
  entity: 'c',
  property: 'name',
}, {
  label: 'Invalid',
  entity: 'a',
  // Type error: 'foo' is not assignable to 'id' | 'name' | 'uuid' | 'description'
  property: 'foo',
},
// Type error: 'id' is not assignable to 'uuid' | 'description'
{
  label: 'Invalid',
  entity: 'b',
  property: 'id',
}];

游乐场链接


推荐阅读