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
Aleksey-Bykov It's not in favor to control distributiveness, it just happens to do so (and is the simplest syntax for it). [T] is a tuple containing one element of type T just as it would be in any other context. A conditional type distributes only over naked type parameters, since the condition is now on a tuple that contains T it is no longer over a naked type parameter and hence distribution does not happen.
This would happen with any other usage of T , for example this would also prevent distribution:
type NoDistribution<T> = { o: T } extends { o: number } ? "Y" : "N"
type T1 = NoDistribution<number> //Y
type T2 = NoDistribution <number | string> //N
Tuples just happen to be the most concise way to do this. (Also for the type relation to be equivalent to T extends U the usage of T would have to be in a covariant position. A contravariant position, such as a function parameter, would inverse the relation)
I would also be reluctant to assign any special meaning to () in conditionals. () can already be used in types for grouping and they do not add any meaning they just change precedence (as they do in most other context): type A = (readonly number[])[].
Tuples do have special treatment in that they correctly create substitution types in the true branch, effectively doing type narrowing---an object type will not do that. This is why the tuple method is proposed as the canonical way of doing this.
I would also be reluctant to assign any special meaning to () in conditionals. () can already be used in types for grouping and they do not add any meaning they just change precedence (as they do in most other context): type A = (readonly number[])[].
The issue I have is that this really isn't a syntactic problem; the issue is with discoverability of semantics.
People start by seeing the conditional types in the lib such as:
typeExclude<T,U>=TextendsU ? never : T;
that seems to just work. Then they try something abit different, or more advance, and they get bitten by using a conditional type that was distributive when it should not have been.
The problem is not how the distinction is represented; the problem is just signalling that such a distinction even exists.
If I were being obtuse I would say that people should just read the handbook before trying advanced stuff. It currently doesn't mention the tuple trick, which I think it should, (Maybe I'll add a PR), but it does explain a lot of the details.
A radical suggestion would be to change conditional types to say that they are always distributive: this is just how they get resolved. In particular, when the check type is a union the conditional type is distributed. This does not happen at instantiation, rather at resolution. The main change would be things like:
typeUndefinedIs<T>=(undefined|number)extendsT ? true : false;typeA=UndefinedIs<number>;// was false, now booleantypeNeverIs<T>=neverextendsT ? true : false;typeB=NeverIs<number>// was true, now never;
The same tuple trick would work to prevent distribution because it fixes the check type as a non union. The main difference is now that the behaviour is consistent between substitution (beta reduction of types), and is no longer a property fixed at the declaration. I'm not sure I like this though, and it probably has fundamental problems.
but why tuples? it looks like a random work of god, somehow tuples is what can save us, and it's a miracle indeed, but now that we know how it was done and why it was done in tuples, why won't we learn the lesson and make it a first class feature that
originated with tuples (and we pay it all due respect and all zealots are free to use tuples for it if they think it's a more "true" way)
but also now can be done without even mentioning them
because, let's be clear, tuples are orthogonal to the problem of distributivness
because if we don't, and stick with tuples, it will give tuples a superpower that they do not deserve, and in a long run we will regret this
Distributive conditional types seem very similar conceptually to mapped types. Perhaps the syntax for distributive conditional types could be improved by adopting a more general syntax for mapping union types to create other union types that borrows from the mapped types in operator. (scroll to the proposal section for more detail on this.)
Long Version
After a lot of searching to make sure I wasn't duplicating an existing suggestion, I came across this issue. Hopefully this is the appropriate place to post. I'm not sure I have a great suggestion for how to improve distributive conditionals but I'll take a stab at it. I also don't have much in the way of formal computer science training, so forgive me if I come off as a bit sophomoric.
I was reading this article in an attempt to implement strongly typed event emitters. After reading through the Advanced Types section of the Handbook, I was finally able to understand this type alias:
export type MatchingKeys<
TRecord,
TMatch,
K extends keyof TRecord = keyof TRecord
> = K extends (TRecord[K] extends TMatch ? K : never) ? K : never;
First, it's not clear to me why a naked type parameter should be distributive while a wrapped type parameter is not. If the parenthesized conditional type was distributive, then there would be no need for nested conditional types in this type alias. TRecord[K] could be resolved to the type of each of the constituents of K when applied to TRecord, and MatchingKeys<TRecord, TMatch> would represent a type whose properties each have a type of TMatch.
Second, the parentheses imply that the inner conditional type will be resolved first, at which point the outer conditional type will be distributive over either K or never, depending on the result of evaluating that inner conditional type.
Third, it seems to me that the syntax for mapped and distributive conditional types represent a redundancy and an opportunity to improve the latter by adopting something similar to the former. Conceptually, mapping the members of a type to members of a new type seems very similar to distributing a conditional type over a union.
Proposal
What if TypeScript instead had a more generalized way to map over the constituents of a union to produce a new union? The in operator could be overloaded to define a new type parameter that represents a constituent of the union. Modifying an example from the Handbook:
type BoxedValue<T> = { value: T };
type Boxed<T, M in T> = BoxedValue<M>
type T20 = Boxed<string | number>; // BoxedValue<string> | BoxedValue<number>
Even if T is not a union, TypeScript could treat it like a union consisting of one constituent, so this generic type alias could work for non-union types.
Conditional distributive types would then become just a special case of this new "distributive type" concept. Again modifying an example from the Handbook:
type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T, M in T> = M extends any[] ? BoxedArray<M[number]> : BoxedValue<T>;
type T22 = Boxed<string | number[]>; // BoxedValue<string> | BoxedArray<number>;
The example from the article cited above could then be dramatically simplified:
export type MatchingKeys<
TRecord,
TMatch,
K extends keyof TRecord = keyof TRecord,
M in K
> = TRecord[M] extends TMatch ? M : never;
This makes it explicit that M does not represent the union, but each constituent of the union.
(Unexpected) Distributivity of conditional types is something that bit me fairly strongly today, the fact some features are just magically distributive seems fairly confusing design in my opinion.
Like the main reason distributivity is wanted is so that people can specify things like Boxed<a | b> === Box<a> | Box<b>, but that seems more of a failure of the language to provide a way to specify a list of types to be distributed later.
Consider arrays, Array<A | B> is not Array<A> | Array<B>, the fact some generics just magically distribute seems more of an confusing feature than a predictable one. It'd be more obvious if there was just a way to declare a distributed union for any generic, i.e. maybe Array<A || B> === Array<A> | Array<B>. People then wouldn't even need to define the wrappers, they could just || wherever they want spreading over a type constructor.
I can handle the confusion around distributiveness in conditional types, but how am I supposed to control distributiveness in mapped types?
Consider the following case:
typeUnion={foo: 1}|{foo: 2}typeGetters<T>={[KinkeyofT]: ()=>T[K]}typeActualFooGetter=Getters<Union>['foo']// (() => 1) | (() => 2)typeDesiredFooGetter=()=>Union['foo']// () => 1 | 2// How do I get Getters<Union> to be { foo: () => Union['foo'] }?
Any suggestions would be much appreciated.
Also, I don't see anything in the documentation explaining that mapped types are distributive, so that would be a welcome addition. Although, maybe I am just looking in the wrong spot.
As a side note, I really like the proposal from Daniel Arant (@SanzSeraph). Essentially a special case of a local type alias (e.g. as described in #41470) that would be used to create distributive types. I think that would make distributiveness way more accessible for most developers
Sander Altman (@saltman424) Distributivity for mapped types requires mapping over directly keyof T or a type parameter K that has a constraint of keyof T. If you don't map directly over those, distributivity is disable:
Titian Cernicova-Dragomir (@dragomirtitian) thank you!! That is really helpful to know. Do you know if that is anywhere in the documentation? If not, that would be a great addition.
I'm going to be the grouch here, even though I've understood distributive conditional types and how to defeat distribution with [ ] for a long time, this still drives me up a wall.
Mostly, I do love TypeScript. Having some way of distributing is definitely better than nothing. But having an awkward way really gets in the way of me completely loving it. (And it's sad to see that Flow copied the same design instead of coming up with a cleaner one...)
It flies in the face of all common sense that if you have type A<T> = ..., you can't substitute the right-hand side in place of the left hand side to determine the computed type. We're used to substitution in algebra, or in lambda calculus; it's unusual that type aliases don't follow this same substitution logic. They can't even properly be called type aliases! Because they do a lot more than simply creating an alias for another type. This is my gripe with tuple mapping as well. Type aliases shouldn't do anything special!
But aside from taste, the objective drawback is that it's bound to confuse all newcomers to the language. You can't tell me anyone understood what the brackets in [T] extends [number] meant the first time they saw it. Or that it was obvious they should use the bizarre extends any ? when they need to unconditionally distribute.
So a more obvious syntax would be a huge win for everyone, not just for crotchety devs like me who can't get over their raging pet peeves about it.
There should be an inline syntax for distributing over a union anywhere, not just in a type alias parameter declaration like Daniel Arant (@SanzSeraph) suggested.
const x: { foo: T in 1 | 2 | 3 as { value: T } } = ...
This would increase everyone's productivity. Having to define a type alias and pick a name for it interrupts your train of thought.
Activity
RyanCavanaugh commentedon Mar 25, 2019
Proposal: allow wrapping the type in
[]to indicate non-distributivity, e.g. write🤷♂️
zpdDG4gta8XKpMCd commentedon Mar 25, 2019
this already has some weird meaning: #29662 (comment)
and besides: #9217 (comment)
how about we make some new bold syntax for the type domain like
together Tandapart Tjack-williams commentedon Mar 25, 2019
My ad-hoc comment is a short-term solution for a yet to be confirmed bug. It definitely isn't meaningful.
nevir commentedon Apr 4, 2019
Would wrapping in
()make sense? Conceptually, it's pretty close to operator precedence and how we manage itzpdDG4gta8XKpMCd commentedon Apr 5, 2019
there was some hold back on using
(...)syntax for special purposes: #9217 (comment)not sure why
[...]is in favor nowdragomirtitian commentedon Apr 5, 2019
Aleksey-Bykov It's not in favor to control distributiveness, it just happens to do so (and is the simplest syntax for it).
[T]is a tuple containing one element of typeTjust as it would be in any other context. A conditional type distributes only over naked type parameters, since the condition is now on a tuple that containsTit is no longer over a naked type parameter and hence distribution does not happen.This would happen with any other usage of
T, for example this would also prevent distribution:Tuples just happen to be the most concise way to do this. (Also for the type relation to be equivalent to
T extends Uthe usage ofTwould have to be in a covariant position. A contravariant position, such as a function parameter, would inverse the relation)I would also be reluctant to assign any special meaning to
()in conditionals.()can already be used in types for grouping and they do not add any meaning they just change precedence (as they do in most other context):type A = (readonly number[])[].jack-williams commentedon Apr 5, 2019
Tuples do have special treatment in that they correctly create substitution types in the true branch, effectively doing type narrowing---an object type will not do that. This is why the tuple method is proposed as the canonical way of doing this.
Very much agree.
zpdDG4gta8XKpMCd commentedon Apr 5, 2019
i totally get it that
[...]happened to do the trick, what i am saying is that although it does what we need:so when they say, "hey, let's just officially use it", i say there must be a better way
jack-williams commentedon Apr 5, 2019
The issue I have is that this really isn't a syntactic problem; the issue is with discoverability of semantics.
People start by seeing the conditional types in the lib such as:
that seems to just work. Then they try something abit different, or more advance, and they get bitten by using a conditional type that was distributive when it should not have been.
The problem is not how the distinction is represented; the problem is just signalling that such a distinction even exists.
If I were being obtuse I would say that people should just read the handbook before trying advanced stuff. It currently doesn't mention the tuple trick, which I think it should, (Maybe I'll add a PR), but it does explain a lot of the details.
A radical suggestion would be to change conditional types to say that they are always distributive: this is just how they get resolved. In particular, when the check type is a union the conditional type is distributed. This does not happen at instantiation, rather at resolution. The main change would be things like:
The same tuple trick would work to prevent distribution because it fixes the check type as a non union. The main difference is now that the behaviour is consistent between substitution (beta reduction of types), and is no longer a property fixed at the declaration. I'm not sure I like this though, and it probably has fundamental problems.
zpdDG4gta8XKpMCd commentedon Apr 5, 2019
but why tuples? it looks like a random work of god, somehow tuples is what can save us, and it's a miracle indeed, but now that we know how it was done and why it was done in tuples, why won't we learn the lesson and make it a first class feature that
because, let's be clear, tuples are orthogonal to the problem of distributivness
because if we don't, and stick with tuples, it will give tuples a superpower that they do not deserve, and in a long run we will regret this
9 cages and 10 pigeons, let's make another cage
SanzSeraph commentedon Mar 26, 2020
TL;DR;
Distributive conditional types seem very similar conceptually to mapped types. Perhaps the syntax for distributive conditional types could be improved by adopting a more general syntax for mapping union types to create other union types that borrows from the mapped types
inoperator. (scroll to the proposal section for more detail on this.)Long Version
After a lot of searching to make sure I wasn't duplicating an existing suggestion, I came across this issue. Hopefully this is the appropriate place to post. I'm not sure I have a great suggestion for how to improve distributive conditionals but I'll take a stab at it. I also don't have much in the way of formal computer science training, so forgive me if I come off as a bit sophomoric.
I was reading this article in an attempt to implement strongly typed event emitters. After reading through the Advanced Types section of the Handbook, I was finally able to understand this type alias:
First, it's not clear to me why a naked type parameter should be distributive while a wrapped type parameter is not. If the parenthesized conditional type was distributive, then there would be no need for nested conditional types in this type alias. TRecord[K] could be resolved to the type of each of the constituents of K when applied to TRecord, and MatchingKeys<TRecord, TMatch> would represent a type whose properties each have a type of TMatch.
Second, the parentheses imply that the inner conditional type will be resolved first, at which point the outer conditional type will be distributive over either K or never, depending on the result of evaluating that inner conditional type.
Third, it seems to me that the syntax for mapped and distributive conditional types represent a redundancy and an opportunity to improve the latter by adopting something similar to the former. Conceptually, mapping the members of a type to members of a new type seems very similar to distributing a conditional type over a union.
Proposal
What if TypeScript instead had a more generalized way to map over the constituents of a union to produce a new union? The
inoperator could be overloaded to define a new type parameter that represents a constituent of the union. Modifying an example from the Handbook:Even if T is not a union, TypeScript could treat it like a union consisting of one constituent, so this generic type alias could work for non-union types.
Conditional distributive types would then become just a special case of this new "distributive type" concept. Again modifying an example from the Handbook:
The example from the article cited above could then be dramatically simplified:
This makes it explicit that M does not represent the union, but each constituent of the union.
Jamesernator commentedon Apr 8, 2022
(Unexpected) Distributivity of conditional types is something that bit me fairly strongly today, the fact some features are just magically distributive seems fairly confusing design in my opinion.
Like the main reason distributivity is wanted is so that people can specify things like
Boxed<a | b> === Box<a> | Box<b>, but that seems more of a failure of the language to provide a way to specify a list of types to be distributed later.Consider arrays,
Array<A | B>is notArray<A> | Array<B>, the fact some generics just magically distribute seems more of an confusing feature than a predictable one. It'd be more obvious if there was just a way to declare a distributed union for any generic, i.e. maybeArray<A || B> === Array<A> | Array<B>. People then wouldn't even need to define the wrappers, they could just||wherever they want spreading over a type constructor.saltman424 commentedon May 11, 2023
I can handle the confusion around distributiveness in conditional types, but how am I supposed to control distributiveness in mapped types?
Consider the following case:
Any suggestions would be much appreciated.
Also, I don't see anything in the documentation explaining that mapped types are distributive, so that would be a welcome addition. Although, maybe I am just looking in the wrong spot.
As a side note, I really like the proposal from Daniel Arant (@SanzSeraph). Essentially a special case of a local type alias (e.g. as described in #41470) that would be used to create distributive types. I think that would make distributiveness way more accessible for most developers
dragomirtitian commentedon May 11, 2023
Sander Altman (@saltman424) Distributivity for mapped types requires mapping over directly
keyof Tor a type parameterKthat has a constraint ofkeyof T. If you don't map directly over those, distributivity is disable:Playground Link
saltman424 commentedon May 11, 2023
Titian Cernicova-Dragomir (@dragomirtitian) thank you!! That is really helpful to know. Do you know if that is anywhere in the documentation? If not, that would be a great addition.
jedwards1211 commentedon Nov 10, 2023
I'm going to be the grouch here, even though I've understood distributive conditional types and how to defeat distribution with [ ] for a long time, this still drives me up a wall.
Mostly, I do love TypeScript. Having some way of distributing is definitely better than nothing. But having an awkward way really gets in the way of me completely loving it. (And it's sad to see that Flow copied the same design instead of coming up with a cleaner one...)
It flies in the face of all common sense that if you have
type A<T> = ..., you can't substitute the right-hand side in place of the left hand side to determine the computed type. We're used to substitution in algebra, or in lambda calculus; it's unusual that type aliases don't follow this same substitution logic. They can't even properly be called type aliases! Because they do a lot more than simply creating an alias for another type. This is my gripe with tuple mapping as well. Type aliases shouldn't do anything special!But aside from taste, the objective drawback is that it's bound to confuse all newcomers to the language. You can't tell me anyone understood what the brackets in
[T] extends [number]meant the first time they saw it. Or that it was obvious they should use the bizarreextends any ?when they need to unconditionally distribute.So a more obvious syntax would be a huge win for everyone, not just for crotchety devs like me who can't get over their raging pet peeves about it.
jedwards1211 commentedon Nov 10, 2023
There should be an inline syntax for distributing over a union anywhere, not just in a type alias parameter declaration like Daniel Arant (@SanzSeraph) suggested.
This would increase everyone's productivity. Having to define a type alias and pick a name for it interrupts your train of thought.