typescript - 如何将密钥与先前省略的密钥类型合并回来?
问题描述
我正在尝试实现一个通用的 inMemoryGateway 构建器。我在创建实现时遇到了打字问题:我希望能够提供没有“id”的实体(使用 typescript 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())
解决方案
这里的基本问题与this question中关于将值分配给 a Partial<T>
whenT
是扩展某些已知对象类型的通用参数的问题相同U
。您不能只返回 type 的值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!
如果你想安全地重写你的代码,你可以通过保持你创建的实际类型:not E
, but来降低代码的可读性和复杂性Omit<E, "id"> & {id: string}
。这总是正确的,即使原始的属性E
类型更窄: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!
但这不起作用,因为编译器并没有真正费心尝试分析未解析的条件类型的交集,例如Omit<E, "id">
. 有一个建议可以解决这个问题,但现在你需要一个类型断言。
无论如何,我希望您在这里使用的方式是使用类型断言,但希望上面的解释显示了编译器在做什么。希望有帮助;祝你好运!
推荐阅读
- windows - 无法启用 WSL(适用于 Linux 的 Windows 子系统)
- laravel - Laravel 雄辩的查询运行两次
- python-3.x - 在 Windows 10 中安装 Anaconda 3 python 3.6 版本
- php - PHP 工匠从 /database/seeds 中的新文件夹运行播种机
- ansible - 无法使用 ANSIBLE 执行 bashrc 函数
- php - 单击联系表单按钮时,站点将重定向到自定义帖子条目
- xml - XSLT 仅复制某些节点
- linux - 如果输入的第一行包含字符串,如何使用 sed 或 grep 删除它?
- arrays - fnDrawCallback 如何循环遍历每一行
- php - PHPWord - 损坏的 Word 文件