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

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

Closed
SHND opened this issue Jan 15, 2025 · 4 comments
Closed
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@SHND
Copy link

SHND commented Jan 15, 2025

🔎 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
@SHND
Copy link
Author

SHND commented Jan 15, 2025

@jcalz
Copy link
Contributor

jcalz commented Jan 15, 2025

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.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jan 15, 2025
@SHND
Copy link
Author

SHND commented Jan 16, 2025

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

@RyanCavanaugh , @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?

@jcalz
Copy link
Contributor

jcalz commented Jan 16, 2025

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.

@SHND SHND closed this as completed Jan 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

3 participants