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:
type A<T extends string> = { a: T };
type B<T> = A<T> | { b: number }
let x: B<string> // Should infer to type '{ a: string } | { b: number }'
let y: B<number> // Should infer to type '{ b: number }'
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:
type A<T extends string> = { a: T };
type B<T extends number> = { b: T };
type C<T extends string | number> = A<T> | B<T>
let x: C<string> // Should infer to type '{ a: string }'
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.
Its looks like meta-programming, to force compiler to choose (evaluate) the type.
@olegdunkan
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 {a:T} | {b:T} | {c:T}
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 T1<U> | T2<U> once the generic type is known.
))) I see, third case, discriminated union is T1<T> | T2<T> | ... | Tn<T>
But notion of the discriminated union supposes that resulting type of T1<T> | T2<T> | ... | Tn<T> will be some (only one) of them.
Conclude, for each Tn the compiler checks if type argument respects Tn's constraint then include Tn in the union.
But last question, how does the compiler understand that user wants to implement a discriminated union?
Because now it is not valid code:
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 discriminated union union type is. the "|" symbol in TypeScript denotes one basically. I'm not asking for a new feature, just some extra leniency on one end and a new type narrowing rule on the other.
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 B worries me, because it breaks the intuition that T is valid for each of the constituents. A more straightforward approach has been proposed by @isiahmeadows in https://github.com/Microsoft/TypeScript/issues/13257, however its very very complicated. A less ambitions proposal might be a better start.
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.
Edit: Regarding my proposal in #13257.
@gcnew Yeah, and I should really re-work it enough to make the semantics a little clearer (it acts like it'll either match or error completely, when in reality, it should match or just fail, reusing that error if it's reasonably close).
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 Default<T> to anything but number, but that wouldn't work in every case. I guess if we had #4183 that might be generally possible, but it would very quickly get large and messy.
I think overload style switching on the first match is more useful.
@gcnew:
Generally, I'm in two minds about type level pattern matching.
Though not exposed to utilize from the type level yet, isn't that what function overloading already does?
@SimonMeskens:
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 never? That would be unfortunate, as we could no longer see any of the errors we're getting.
So yeah, I share @gcnew's concerns on this proposal.
A combination of union iteration + 6606 (overloads to swallow errors) could potentially solve something like that, by ensuring it wouldn't come to actual errors. Both seem far off though. Edit: sorry, overloads only swallow "your type doesn't match this type" errors, doesn't protect from "your thing already errored".
As a simpler alternative though, with just type-level type-checks (also 6606) one might also be able to conditionally branch based on whether types would match the required inputs, e.g. type B<T> = If<Matches<T, string>, A<T>, never> | { b: number }.
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.
Most helpful comment