我正在尝试实现通用的inMemoryGateway构建器。我在create实现上遇到打字问题:我希望能够提供一个没有'id'的实体(使用打字稿Omit),而不是添加缺少的'id'。但是这些类型似乎不兼容。我现在使用as any
,但有人会看到更干净的解决方案吗?
interface EntityGateway<E extends {id: string}> {
create: (entity: Omit<E, 'id'>) => E
getAll: () => E[]
}
const buildInMemoryGateway = <Entity extends {id: string}>(): EntityGateway<Entity> => {
const entities: Entity[] = [];
return {
create: (entityWithoutId: Omit<Entity, 'id'>) => {
const entity: Entity = { ...entityWithoutId, id: 'someUuid' }
// Error here on entity :
// Type 'Pick<Entity, Exclude<keyof Entity, "id">> & { id: string; }' is not assignable to type 'Entity'.
// ugly fix: const entity: Entity = { ...entityWithoutId as any, id: 'someUuid' }
entities.push(entity);
return entity
},
getAll: () => {
return entities;
}
}
}
interface Person {
id: string,
firstName: string,
age: number,
}
const personGateway = buildInMemoryGateway<Person>();
personGateway.create({ age: 35, firstName: 'Paul' }); // OK as expected
personGateway.create({ age: 23, whatever: 'Charlie' }); // error as expected
console.log("Result : ", personGateway.getAll())
答案 0 :(得分:1)
当Partial<T>
是扩展某些已知对象类型T
的通用参数时,此处的基本问题与this question中的有关为U
分配值的问题相同。您不能只返回类型为Partial<U>
的值,因为在T extends U
时,可以通过向U
添加新属性(没问题)或通过变窄 T
的现有属性(嗯!)。而且,由于调用方在泛型函数中选择类型参数,因此该实现无法保证T
的属性在类型上不会比U
的相应属性窄。
这会导致此问题:
interface OnlyAlice { id: "Alice" };
const g = buildInMemoryGateway<OnlyAlice>();
g.create({});
g.getAll()[0].id // "Alice" at compile time, "someUuid" at runtime. Uh oh!
如果您想安全地重写代码,则可以通过保持所创建的实际类型,使代码的可读性和复杂性降低,而不是E
,而是Omit<E, "id"> & {id: string}
。即使原始的E
的{{1}}属性的类型更窄,也总是如此:
id
对于您的示例,其行为相同:
type Stripped<E> = Omit<E, "id">;
type Entity<E> = Stripped<E> & { id: string };
interface EntityGateway<E> {
create: (entity: Stripped<E>) => Entity<E>
getAll: () => Entity<E>[]
}
const buildInMemoryGateway = <E>(): EntityGateway<E> => {
const entities: Entity<E>[] = [];
return {
create: (entityWithoutId: Stripped<E>) => {
const entity = { ...entityWithoutId, id: 'someUuid' }
entities.push(entity);
return entity
},
getAll: () => {
return entities;
}
}
}
但是对于上面的病理示例,它的行为有所不同:
interface Person {
id: string,
firstName: string,
age: number,
}
const personGateway = buildInMemoryGateway<Person>();
personGateway.create({ age: 35, firstName: 'Paul' }); // OK as expected
personGateway.create({ age: 23, whatever: 'Charlie' }); // error as expected
如果您阅读并对自己说:“哦,别这样,没有人会把interface OnlyAlice { id: "Alice" };
const g = buildInMemoryGateway<OnlyAlice>();
g.create({});
g.getAll()[0].id // string at compile time, "someUuid" at run time, okay!
属性缩小为字符串文字”,这很公平。但这意味着您需要使用类似类型断言的东西,如您所见:
id
您可能希望编译器认为这是可以接受的:
const entity = { ...entityWithoutId, id: 'someUuid' } as E; // assert
但这不起作用,因为编译器实际上并没有尝试分析诸如 const entity: E = { ...entityWithoutId, id: 'someUuid' as E["string"]}; // error!
之类的未解析条件类型的交集。有一个suggestion可以解决这个问题,但是现在您需要类型断言。
无论如何,我希望您要使用的方法是使用类型断言,但是希望上面的解释可以显示编译器在做什么。希望能有所帮助;祝你好运!