-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
syntax to control over distributivness #30572
Comments
Proposal: allow wrapping the type in type C<T> = [T] extends [U] ? V : W; 🤷♂️ |
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 |
My ad-hoc comment is a short-term solution for a yet to be confirmed bug. It definitely isn't meaningful. |
Would wrapping in |
there was some hold back on using not sure why |
@Aleksey-Bykov It's not in favor to control distributiveness, it just happens to do so (and is the simplest syntax for it). This would happen with any other usage of
Tuples just happen to be the most concise way to do this. (Also for the type relation to be equivalent to I would also be reluctant to assign any special meaning to |
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. type OnlyNumber<N extends number> = [N];
type NoDistribution<T> = { o: T } extends { o: number } ? OnlyNumber<T> : "N" // error
type NoDistribution2<T> = [T] extends [number] ? OnlyNumber<T> : "N" // no error
Very much agree. |
i totally get it that
so when they say, "hey, let's just officially use it", i say there must be a better way |
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: type Exclude<T, U> = T extends U ? 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: type UndefinedIs<T> = (undefined | number) extends T ? true : false;
type A = UndefinedIs<number>; // was false, now boolean
type NeverIs<T> = never extends T ? true : false;
type B = 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
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 |
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 Long VersionAfter 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. ProposalWhat if TypeScript instead had a more generalized way to map over the constituents of a union to produce a new union? The
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. |
(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 Consider arrays, |
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: type Union = { foo: 1 } | { foo: 2 }
type Getters<T> = { [K in keyof T]: () => T[K] }
type ActualFooGetter = Getters<Union>['foo'] // (() => 1) | (() => 2)
type DesiredFooGetter = () => 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 @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 |
@saltman424 Distributivity for mapped types requires mapping over directly type Union = { foo: 1 } | { foo: 2 }
type Getters<T> = { [K in keyof T & {}]: () => T[K] }
type ActualFooGetter = Getters<Union>['foo'] // () => 1 | 2
type DesiredFooGetter = () => Union['foo'] // () => 1 | 2? |
@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 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 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 @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. |
from: #30569 (comment)
so basically we need a way to say it clear and loud in the language whether we want:
or
as a result of a type operation
The text was updated successfully, but these errors were encountered: