【问题标题】:How to merge back a key with a type where the key was previously Omitted?如何将密钥与先前省略的密钥类型合并回来?
【发布时间】:2019-11-23 01:21:58
【问题描述】:

我正在尝试实现一个通用的 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())

【问题讨论】:

  • 您对interface OnlyAlice { id: "Alice" }; const g = buildInMemoryGateway&lt;OnlyAlice&gt;(); g.create({}); g.getAll()[0].id 有什么期望?
  • 我希望得到'someUuid',但这是我不会有的用法......
  • 但是该值的类型在类型系统中是"Alice"。你所做的并不安全,因为Omit&lt;E, "id"&gt; &amp; {id: string} 不一定与E 相同,即使E extends {id: string} 也是如此。这就是编译器警告您的原因。如果您不希望 id 永远是字符串文字类型,那么在您正在做的时候使用类型断言并继续前进。否则,有一些方法可以使其类型安全,但这很丑陋,因为您必须将 Omit&lt;E, "id"&gt; &amp; {id: string} 之类的类型拖到以前使用 E 的地方。
  • 问题与this question中的问题基本相同。

标签: typescript typescript-typings


【解决方案1】:

这里的基本问题与this question 中的相同,即当T 是扩展某些已知对象类型U 的通用参数时,将值分配给Partial&lt;T&gt;。您不能只返回Partial&lt;U&gt; 类型的值,因为当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&lt;E, "id"&gt; &amp; {id: string}。这始终是正确的,即使原始 Eid 属性的类型更窄:

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&lt;E, "id"&gt; 这样的未解析条件类型的交集。有一个 suggestion 来解决这个问题,但现在你需要一个类型断言。


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

Link to code

【讨论】:

    猜你喜欢
    • 2021-08-09
    • 1970-01-01
    • 1970-01-01
    • 2017-05-09
    • 2020-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多