如何将密钥与先前省略密钥的类型合并回来? [英] How to merge back a key with a type where the key was previously Omitted?

查看:23
本文介绍了如何将密钥与先前省略密钥的类型合并回来?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试实现一个通用的 inMemoryGateway 构建器.我在创建实现上有一个打字问题:我希望能够提供一个没有id"的实体(使用 typescript Omit),而不是添加缺少的id".但这些类型似乎不兼容.我现在使用 as any 但有人会看到更简洁的解决方案吗?

I am trying to implement a generic inMemoryGateway builder. I have a typing problem on the create implementation: I want to be able to give an entity without the 'id' (using typescript Omit) and than to add the missing 'id'. But the types don't seem compatibles. I used as any for now but anyone would see a cleaner solution ?

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())

推荐答案

这里的基本问题与 这个问题关于当 T 是一个 Partial扩展某些已知对象类型 U 的通用参数.您不能只返回 Partial 类型的值,因为当 T extends U 时,它可以通过向 U 添加新属性来实现>(没问题),或者通过缩小T 的现有属性(呃,哦!).并且由于在泛型函数中调用者选择类型参数,实现不能保证T的属性在类型上不会比的相应属性更窄U.

The fundamental issue here is the same as in this question about assigning a value to a Partial<T> when T is a generic parameter extending some known object type U. You can't just return a value of type Partial<U>, because when T extends U it could do so by adding new properties to U (no problem), or by narrowing the existing properties of T (uh oh!). And since in a generic function the caller chooses the type parameter, the implementation cannot guarantee that properties of T won't be narrower in type than the corresponding properties of 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,而是 省略<E,id">&{id:字符串}.这总是正确的,即使原始 Eid 属性具有更窄的类型:

If you wanted to rewrite your code safely, you could do so by making the code less readable and more complex, by keeping the actual type you've created: not E, but Omit<E, "id"> & {id: string}. That is always true, even if the original E has a narrower type for its id property:

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;
        }
    }
}

对于您的示例,其行为相同:

And that behaves the same for your examples:

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

但是现在对于上面的病理示例,它的行为有所不同:

But now it behaves differently for the pathological example above:

interface OnlyAlice { id: "Alice" };
const g = buildInMemoryGateway<OnlyAlice>();
g.create({});
g.getAll()[0].id // string at compile time, "someUuid" at run time, okay!

<小时>

如果你读到它并对自己说,哦,拜托,没有人会把 id 属性缩小到字符串文字的范围内",这是公平的.但这意味着您需要使用类型断言之类的东西,如您所见:


If you read that and said to yourself, "oh come on, nobody's going to narrow the id property to a string literal", that's fair. But it means you need to use something like a type assertion, as you saw:

 const entity = { ...entityWithoutId, id: 'someUuid' } as E; // assert

可能期望编译器可以认为这是可以接受的:

You might expect that the compiler could see this as acceptable:

 const entity: E = { ...entityWithoutId, id: 'someUuid' as E["string"]}; // error!

但这行不通,因为编译器并没有真正费心去分析像 Omit<E, "id"> 这样的未解析条件类型的交集.有一个建议来解决这个问题,但现在你需要一个类型断言.

but that doesn't work because the compiler doesn't really bother trying to analyze the intersection of an unresolved conditional type like Omit<E, "id">. There's a suggestion to address that but for now you need a type assertion.

无论如何,我希望您在这里的方式是使用类型断言,但希望上面的解释显示了编译器正在做什么.希望有所帮助;祝你好运!

Anyway I'd expect the way you want to go here is to use a type assertion, but hopefully the explanation above shows what the compiler is doing. Hope that helps; good luck!

链接到代码

这篇关于如何将密钥与先前省略密钥的类型合并回来?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆