Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
fcole90 opened this issue Feb 6, 2021 · 5 comments
Labels
Docs The issue relates to how you learn TypeScript

Comments

@fcole90
Copy link

fcole90 commented Feb 6, 2021

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.

@fcole90 fcole90 changed the title Interface extension with omit produce incoherent results: (C | D) \ { 'a' } != (C \ { 'a' }) | (D \ { 'a' }) Interface extension with omit produce incoherent results: Omit<C | D, 'a'> !== Omit<C, 'a'> | Omit<D, 'a'> Feb 6, 2021
@MartinJohns
Copy link
Contributor

keyof T returns all common properties for union types (by design), and Omit<T, K> is using keyof T. As a result your type BasicPlusCOrDMinusA only contains the common property b.

You don't have this issue in your getUnknownBis function because here you don't use a union type with Omit<T, K>, resulting in all properties of T being present with keyof T.

@MartinJohns
Copy link
Contributor

MartinJohns commented 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.

@fcole90
Copy link
Author

fcole90 commented Feb 6, 2021

keyof T returns all common properties for union types (by design)

Hence, any union that is passed as the first type to Omit, is being considered as an intersection instead, as I was supposing 🤔

I think this is something that needs to be documented, as it's very very counterintuitive, and nothing allows to think so in the docs: https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys
Would I need to open a different bug to suggest an update to the docs or this can handle that?

@fcole90
Copy link
Author

fcole90 commented 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 😊 👍

@fcole90
Copy link
Author

fcole90 commented 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Docs The issue relates to how you learn TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants