Typescript 接口 属性 作为通用对象的嵌套键
Typescript interface property as nested keys of generic objects
基于以下结构:
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
];
使用您定义的接口层次结构,其中 ApiData
和 ApiEntity
被声明为允许 任何字符串 作为 属性 名称,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);
}
但是,如果您去掉 ApiData
和 ApiEntity
,或者至少将它们缩小到不允许所有字符串作为 属性 名称,这个问题就可以解决。
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',
}];
基于以下结构:
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
];
使用您定义的接口层次结构,其中 ApiData
和 ApiEntity
被声明为允许 任何字符串 作为 属性 名称,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);
}
但是,如果您去掉 ApiData
和 ApiEntity
,或者至少将它们缩小到不允许所有字符串作为 属性 名称,这个问题就可以解决。
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',
}];