question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. Itย collects links to all the places you might be looking at while hunting down a tough bug.

And, if youโ€™re still stuck at the end, weโ€™re happy to hop on a call to see how we can help out.

Interface extension with omit produce incoherent results: Omit<C | D, 'a'> !== Omit<C, 'a'> | Omit<D, 'a'>

See original GitHub issue

Bug Report

๐Ÿ”Ž Search Terms

wrong set theory omit

๐Ÿ•— Version & Regression Information

ts-node-dev ver. 1.1.1 (using ts-node ver. 9.1.1, typescript ver. 4.1.3)

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about it
    • ts-node-dev ver. 1.1.1 (using ts-node ver. 9.1.1, typescript ver. 4.1.3)
    • Nightly

โฏ Playground Link

Playground link with relevant code

๐Ÿ’ป Code

interface Basic {
  a: string;
  b: number;
}

interface BasicPlusC extends Basic {
  c: string;
}

interface BasicPlusD extends Basic {
  d: string;
}

type BasicPlusCOrD = BasicPlusC | BasicPlusD;

type BasicPlusCOrDMinusA = Omit<BasicPlusCOrD, 'a'>;

const getUnknown = (): BasicPlusCOrDMinusA => (
  Math.random() > .5 ?
    { b: 0, c: ""} // <-- Causes error
  : { b: 0, d: ""}
);

const getUnknownBis = (): Omit<BasicPlusC, 'a'> | Omit<BasicPlusD, 'a'> => (
  Math.random() > .5 ?
    { b: 0, c: ""}
  : { b: 0, d: ""}
);

const main = (): void => {
  getUnknown();
  getUnknownBis();
};

main();

๐Ÿ™ Actual behavior

Running this code causes the following error:

[INFO] 21:02:26 ts-node-dev ver. 1.1.1 (using ts-node ver. 9.1.1, typescript ver. 4.1.3)
Compilation error in /home/fabio/Projects/aalto/fullstack_open_2020_part9/part_c_patientor/backend/src/delete_me.ts
[ERROR] 21:02:27 โจฏ Unable to compile TypeScript:
src/delete_me.ts:20:13 - error TS2322: Type '{ b: number; c: string; } | { b: number; d: string; }' is not assignable to type 'Pick<BasicPlusCOrD, "b">'.
  Type '{ b: number; c: string; }' is not assignable to type 'Pick<BasicPlusCOrD, "b">'.
    Object literal may only specify known properties, and 'c' does not exist in type 'Pick<BasicPlusCOrD, "b">'.

20     { b: 0, c: ""} // <-- Causes error
               ~~~~~

๐Ÿ™‚ Expected behavior

I would have expected both functions, getUnknown and getUnknownBis to work correctly. This is because Iโ€™m expecting the typing system to follow the same rules as set theory. In this case I have one function, getUnknown, whose return type structure is ({ a, b, c } | ({ a, b, d, }) \ { a }) which means that the resulting structure should be in the form { b, c } | { b, d }.

In the other function, getUnknownBis, the return type structure is ({ a, b, c } \ { a }) | ({ a, b, d, }) \ { a }) which means that the resulting structure should be again in the form { b, c } | { b, d }.

Hence I would expect both to work in the same way, but it looks like the first function is only expecting to return { b }, instead of { b, c } | { b, d }. This makes me think that the type inference engine is doing { b, c } & { b, d } instead.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

3reactions
fcole90commented, Feb 6, 2021

For reference, using the suggested workaround by @IllusionMH, it works as I intended:


// ---------------------------------------------------------------------------------------------------------------------------------------
// By: Andrii Dieiev
// From: https://github.com/microsoft/TypeScript/issues/39556#issuecomment-656925230
type BetterOmit<T, K extends string | number | symbol> = T extends unknown ? Omit<T, K> : never;
// ---------------------------------------------------------------------------------------------------------------------------------------

interface Basic {
  a: string;
  b: number;
}

interface BasicPlusC extends Basic {
  c: string;
}

interface BasicPlusD extends Basic {
  d: string;
}

type BasicPlusCOrD = BasicPlusC | BasicPlusD;

type BasicPlusCOrDMinusA = BetterOmit<BasicPlusCOrD, 'a'>;

const getUnknown = (): BasicPlusCOrDMinusA => (
  Math.random() > .5 ?
    { b: 0, c: ""} // <-- Now works as intended
  : { b: 0, d: ""}
);

const getUnknownBis = (): Omit<BasicPlusC, 'a'> | Omit<BasicPlusD, 'a'> => (
  Math.random() > .5 ?
    { b: 0, c: ""}
  : { b: 0, d: ""}
);

const main = (): void => {
  getUnknown();
  getUnknownBis();
};

main();

Still, I suggest updating the docs to mention this unexpected behaviour and the workaround as well (or even include it in the language with a different name, like UnionOmit, or something alike)

0reactions
fcole90commented, Feb 6, 2021

See also #39556 (search terms union omit in:title). Here you can also find a workaround by providing your own customized version of Omit<> that behaves as you want it.

Thanks a lot, Iโ€™m checking how the workaround works ๐Ÿ˜Š ๐Ÿ‘

Read more comments on GitHub >

github_iconTop Results From Across the Web

TypeScript create Omit type from extended type - Stack Overflow
I want to be able to create a new type DataObject that simply omits the id property of an interface that extends the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found