You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Exclude in typescript behaves differently when substitute with its definition #60973
type_Exclude<T,U>=TextendsU ? never : T;typeMyType1=1|2|3|4typeMyType2=3|4|5|6typeResult1=_Exclude<MyType1,MyType2>// 1 | 2typeResult2=MyType1extendsMyType2 ? never : MyType1// 1 | 2 | 3 | 4typeResult3=[MyType1]extendsMyType2 ? never : MyType1// 1 | 2 | 3 | 4// Maybe an improvement for non-generic cases?typeResult4=[KinMyType1]extendsMyType2 ? 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:
typeMyType1=1|2|3|4typeMyType2=3|4|5|6typeResult4=[KinMyType1]extendsMyType2 ? never : K
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.
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).
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.
Activity
SHND commentedon Jan 15, 2025
Also to backlink, I asked the question in StackOverflow here also: https://stackoverflow.com/questions/79357300/why-exclude-in-typescript-behaves-differently-when-substitute-with-its-definitio
jcalz commentedon 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.
SHND commentedon 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
extendsinline, 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?
jcalz commentedon 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 | 4is fine because numbers can be keys, butDate | string | booleanis 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.