-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Allow union types to pattern match type constraints #17325
Comments
Its looks like meta-programming, to force compiler to choose (evaluate) the type. |
Isn't that already what it does, all the time? The TypeScript compiler is constantly narrowing types, I'm just asking for another narrowing rule, namely that it eliminates impossible types from unions. |
type A<T extends string> = { a: T };
type B<T extends number> = { b: T };
type C<T extends string | number> = { c: T };
type D<T extends string | number> = A<T> | B<T> | C<T>
let x: D<string | number> ; //what to infer? {a:T} | {b:T} or {c:T} And we break constraint rules. |
What do you mean, what to infer? That example doesn't have a narrowing constraint, so no narrowing happens. Result is |
type A<T extends string> = { a: T };
type B<T extends number> = { b: T };
type C<T extends string | number> = { c: T };
type D<T extends string | number> = A<T> | B<T> | C<T>
let x: D<string | number> ; // x: {a:T} | {b:T} | {c:T}
let y: D<string> ; // x: {a:T} | {c:T}
let z: D<number> ; // x: {b:T} | {c:T} |
To understand each other. Do you suggest this form of discriminated unions type T1<T extends U1> = ...;
type T2<T extends U2> = ...;
...
type Tn<T extends Un> = ...;
type U<T extends U1 | U2 | ... | Un> = T1<T> | T2<T> | ... | Tn<T>; or type T1<T extends ...> = U1;
type T2<T extends ...> = U2;
...
type Tn<T extends ...> = Un;
type U<T extends ...> = T1<T> | T2<T> | ... | Tn<T>; where U1 | U2 | ... | Un are discriminated union? |
Neither I think? This first example wouldn't be allowed, because if U is a number, the result would be never. type T1<T extends string> = Array<T>;
type U1<U extends string | number> = T1<U>; // error here, as it is now This second example is allowed, because all possible values of U result in a valid type. type T1<T extends string> = Array<T>;
type T2<T extends number> = Set<number>;
type U1<U extends string | number> = T1<U> | T2<U>; // no error
let x: U1<"foo"> // x: Array<"foo">
let y: U1<4> // x: Set<4>
let z: U1<"bar" | 2> // x: Array<"bar"> | Set<2> The compiler doesn't "pick a type", all it does is narrow the type by eliminating cases from the discriminated union |
))) I see, third case, discriminated union is type A<T extends string> = { a: T };
type B<T extends number> = { b: T };
type C<T extends string | number> = A<T> | B<T>; //error, C<T> constraint must be assignable to A<T> and B<T> constraints |
I think you're confused about what a |
I've replaced all instances of "discriminated union" with just "union type" to make it clearer. Not all union types are discriminated and I think that was causing some confusion. My mistake for not using the right words. Is it clearer now? |
Now, It is clear what you want. Thanks. |
Generally, I'm in two minds about type level pattern matching. On the one hand, it will allow interesting use cases to be properly typed, on the other, it'd add significant complexity. It's not clear how to do type inference, either. type A<T extends string> = { a: T };
type B<T> = A<T> | { b: number } Accepting definitions such as the above PS: Guards should be really simple, otherwise proving that moderately complex guards are total is far from trivial or even impossible - e.g. GADTs meet their match: pattern-matching warnings that account for GADTs, guards, and laziness. |
I don't think this does cover all of the features of #12424. Currently I have a situation where I want to branch a base class to one type, and any children to another. While I have just thought of a way this version could work in that very specific situation, I don't think this provides a general solution for that. Basically, I'd want to be able to match the first matching type, not all. Think about switch fall through and default. Let's say we want to map numbers to strings, and leave everything else untouched. type NumbersToStrings<T extends number> = string;
type Default<T> = T;
type Switched<T> = NumbersToStrings<T> | Default<T>;
// Oops, Switched<number> is string | number We could make that particular example work by exhaustively constraining I think overload style switching on the first match is more useful. |
Though not exposed to utilize from the type level yet, isn't that what function overloading already does? type A<T extends string> = { a: T };
type B<T> = A<T> | { b: number }
let y: B<number> // Should infer to type '{ b: number }' You appear to suggest discarding any union options that turn out to error, is that correct? Every non-union can be considered a unary union. Following that reasoning, what would prevent this proposal from having errors on such 'non-union' (= unary union) types to get converted to the empty union So yeah, I share @gcnew's concerns on this proposal.
|
See, the issue I have with these more complex solutions is that they don't fit the feel of the language. I feel like we need a super simple feature that lets us construct these higher level concepts instead of one feature to rule them all that can do all of the things out of the box. |
Forgive me if this is already one of the proposals, I haven't read through every issue on this topic yet. Thinking about trying to make this fit into the language as raised by @SimonMeskens, what about copying the existing syntax for function overloading, and creating type overloading? type Foo<T extends number> = {aNumber: number};
type Foo<T extends boolean> = {aNumber: boolean};
type Foo<T extends any> = {aDefault: T};
type FooNumber = Foo<number>; // = {aNumber: number};
type FooString = Foo<string>; // = {aDefault: string}; So basically allow overloads for type declarations, and get the type from the first one that's valid. Just like functions. |
I think it would be better to pay the syntactic cost for an orthogonal notation than to introduce a second order syntax. It definitely took me a while to get used to the syntax of Mapped Types but it can be used anywhere. |
I'm not sure. Haskell has an orthogonal notation and I hear that a lot of people wish for a simpler ordered list. I actually like that propo |
Sorry, phone is freaking out. I actually like that proposal a lot |
@SimonMeskens Do you mean the idea I just threw out? I could write it up as a separate issue if there's interest in at least discussing it. |
I'm very interested yeah. It might even make aspects of polymorphism easier to type |
@SimonMeskens Okay, that's cool. I'm focused on some work right now, but I'll see if I can get an issue up for that within the hour. No promises. |
I'm late, but I've posted @17636 for that so we don't keep derailing this issue. |
I'm actually going to close this one, because after looking at all the edge cases, this one plain doesn't work or becomes so complex as to be unusable. Feel free to ask me to open it again if you can fix this proposal and want it over the superior #6606 and #17636 proposals that cover the same ground. |
Not sure if this is a duplicate, I've searched, but couldn't find anything. Basically, allow union types to narrow types to fit constraints. I ran into this issue today, not allowing me to properly type a variable that I could clearly write the type for.
Would also negate the need for #12424 I think.
Specific case with fallbacks:
In this sample, we know there's an option that works regardless of T, so we only include the option with T if the constraint is met.
More general version allows pattern matching:
If the type system were able to solve this case, we'd basically get a really simple version of doing conditionals, type matching at a type level, etc.
Edge cases:
I would not make it order dependent because union types are not ordered. If multiple parts of the union check out, they should all be included. This is not an attempt at writing function overloads in types, though it could potentially be used as such for certain cases.
The text was updated successfully, but these errors were encountered: