Skip to content

Exclude in typescript behaves differently when substitute with its definition #60973

Closed

Description

@SHND

🔎 Search Terms

site:github.com/microsoft/TypeScript Exclude
site:github.com/microsoft/TypeScript Exclude behaves differently
TypeScript Exclude inconsistent
TypeScript Exclude different
TypeScript Exclude behaves differently expanded

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about "Exclude" and "Distributive Conditional Types"

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/C4TwDgpgBAsiAq4IEYoF4qoD5QExRwGYCoAWAKFElgSXw2J1JIFYSA2crq6AfQFEAHgGMANgFcAJhAA88ADRQAqgD50UeFAiDgEAHaSAzsqgB+KHogA3CACcoALg0BubkigAlCIfGjgqDAERCWkZOERIZEVwujUoKAB6BMwSXEp3Lx8-ehoIlC0dfSNcujMLaztHEsj4+KSUnHwiEgoeT29fYGIMAG0YyIBdAt0DY36IfHNLG3sncdRa+uw8EkYyLnqYAEMQACNoLb0oAEsAWzBbAHsbU-1gKAAzS-s9S70AWgBzfTtj4ShhFtDN5TOlqJlOsxegBpE5HeZDbQjYrjSblGZVWGLZL8DweADyHi4QA

💻 Code

type _Exclude<T, U> = T extends U ? never : T;

type MyType1 = 1 | 2 | 3 | 4
type MyType2 = 3 | 4 | 5 | 6

type Result1 = _Exclude<MyType1, MyType2>                   // 1 | 2
type Result2 = MyType1 extends MyType2 ? never : MyType1    // 1 | 2 | 3 | 4
type Result3 = [MyType1] extends MyType2 ? never : MyType1  // 1 | 2 | 3 | 4

// Maybe an improvement for non-generic cases?
type Result4 = [K in MyType1] extends MyType2 ? never : K   // ERROR

🙁 Actual behavior

Using "Distributive Conditional Types" in a generic type behaves differently compared to using concrete types. (Not Referential transparent)

🙂 Expected behavior

"Distributive Conditional Types" always works the same, no matter if it is being used in generic types or not.

Additional information about the issue

If "Distributive Conditional Types" should behave differently, I believe it worth providing a way for non-generic types to be able to distribute over the union type in conditional types, same as how it works in generics. Maybe having a syntax like:

type MyType1 = 1 | 2 | 3 | 4
type MyType2 = 3 | 4 | 5 | 6

type Result4 = [K in MyType1] extends MyType2 ? never : K

Activity

  1. SHND commented on Jan 15, 2025

    @SHND
    Author
  2. jcalz commented on Jan 15, 2025

    @jcalz
    Contributor

    This isn’t a bug, it’s the intended behavior of distributive conditional types. The suggestion at the end is a duplicate of or strongly related to #30572.

  3. SHND commented on Jan 16, 2025

    @SHND
    Author

    Got it! Now that I learned about this, probably I should've opened the ticket as feature request. The part that is bothering me is that, before I was assuming type definitions and generics are Referential Transparent, which makes reasoning about coding at the type level pretty easy.

    I love TypeScript but honestly I'm a bit sad to learning that TypeScript type definitions are not referential transparent. I believe at least TypeScript should provide a way to distribute unions in extends inline, if it's too late to make it Referential Transparent (backward compatibility).

    Ryan Cavanaugh (@RyanCavanaugh) , Joe Calzaretta (@jcalz) Are there any other cases like this that we are not able to expand a generic to its definition? Is currently a way to do this inline?

  4. jcalz commented on Jan 16, 2025

    @jcalz
    Contributor

    As I mentioned in the comment on that question, you could write MyType1 extends infer T1 ? T1 extends MyType2 ? never : T1 : never, but that's not an obvious improvement over just using indirection. For the particular case where your type is keylike (so, 1 | 2 | 3 | 4 is fine because numbers can be keys, but Date | string | boolean is not fine) then you can use a distributive object type (as coined in #47109, it's just a mapped type into which you immediately index) like { [T1 in MyType1]: T1 extends MyType2 ? never : T1 }[MyType1].

    I'm sure interested parties could collect more examples of places where you can't just substitute a type for its definition without changing behavior, but I don't have a big list in mind right now.

  5. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions