-
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
Support open-ended unions #26277
Comments
Open questions here:
|
I think area should be:
In Redux I specify which actions my reducer can handle. But I also end it with an |
Related somewhat: #33471 |
Here's a minimal repro for an issue in flow-sensitivity for open unions: const x = null as any as {
discriminator: string;
} | {
discriminator: 'abc';
extra: string;
};
if (x.discriminator === 'abc') {
console.log(x.extra) // this should not be an type error
} |
@brandonbloom that's a request for negated types IMO. The type there doesn't disallow |
@RyanCavanaugh I've done some digging in to checker.ts and I think there may be a much simpler fix to my specific case. In the |
Upon thinking about this further, I think I understand what you're saying now. You want the discriminator to be "string but not 'abc'". |
Something like this seems to come up quite a lot with string literals. We'll often get a request to support N well-known strings, but in reality, the system allows arbitrary strings. Usually the intent there is that tooling should give auto-complete hints, but the type-checker should not error. This seems to be the case in #42134. Additionally, I've spoken to @bterlson and @jonathandturner about this a couple of times now on the Azure SDK. The work-arounds are
|
Can someone explain why this doesn't work today: type Circle = {
kind: 'circle';
radius: number;
};
type Rectangle = {
kind: 'rectangle';
height: number;
width: number;
};
type UnknownShape = {
kind: Omit<string, 'circle' | 'rectangle'>;
};
type Shape = Circle | Rectangle | UnknownShape;
const f = (s: Shape) => {
switch (s.kind) {
case 'circle':
console.log('circle:', s.radius);
break;
case 'rectangle':
console.log('rectangle:', s.height, s.width);
break;
default:
console.log('unknown:', s);
break;
}
};
f({kind: 'circle', radius: 2.0}); It seems to me that the type checker ought to be able to prove which shape we have in each |
@tibbe It's not possible in TypeScript to describe the type "any string, except these two", so it's not possible to provide the type |
@MartinJohns thanks for the explanation. The context here is parsing (in this particular case using the So we need to be able to create a union that is distinguished based on one field and have the last union member have an "unknown but distinct from the rest" value for the discriminator field. A filed a bug for the concrete parsing problem against io-ts. It should give more context: gcanti/io-ts#665 |
@tibbe This would require "negated types", which is unlikely to come any time soon (aka the next years): #29317 How I would deal with this: Still leave the |
#57943 got me to look at this issue again. I want to brain-dump some ideas before I forget about them, and maybe others can build on them or refine them.
Here's some immediate problems or downsides I would call out with this:
|
@DanielRosenwasser I like where you're going with My bigger concern is about coupling together, in one construct, the documentation/autocomplete portion of this feature with the exhaustiveness checking requirement. On one hand, it would be really great if a type could require that its cases be exhaustively handled everywhere that type is used (unless a particular use site opts-out, like with a cast to On the other hand:
-- To address the naming issue, my proposal would be an intrinsic type like: type WithKnownCases<KnownCases extends BaseType, BaseType> = intrinsic;
I think this would be a useful building block — it would solve a lot of use cases in this issue. However, the utility is still limited a bit without subtraction types. Consider: type Circle = { kind: 'circle'; /* ... */ };
type Rectangle = { kind: 'rectangle'; /* ... */ };
type UnknownShape = { kind: string; };
type Shape = WithKnownCases<Circle | Rectangle, UnknownShape>;
function f(s: Shape) {
switch (s.kind) {
case 'circle':
// What is `s`'s type here?
// Narrowing to `Circle` is probably what people want but, without being able to
// use subtraction types to define `UnknownShape['kind']`, that's unsound.
// So, presumably, `s` is instead `WithKnownCases<Circle, UnknownShape>`.
// That's better than nothing, I guess.
break;
case 'rectangle':
// ...
break;
default:
break;
}
}; Then, for opt-in exhaustiveness checking, analogous to what TS has today, there could be an intrinsic type: type KnownCasesOf<T> = intrinsic; // KnownCasesOf<WithKnownCases<T, ...>> = T Then... function f(s: Shape) {
switch (s.kind) {
case 'circle':
case 'rectangle':
break;
default:
// `s` here is `WithKnownCases<never, UnknownShape>`
assertUnreachable(s as KnownCasesOf<typeof s> satisfies never)
}
}; For opt-out exhaustiveness checking, I'd have a separate intrinsic type: type RequireExhaustiveHandling<T> = intrinsic;
To opt out, there'd be something like: type AllowUnhandledCases<T> = intrinsic; // AllowUnhandledCases<RequireExhaustiveHandling<T>> = T |
Actually, having function f(s: RequireExhaustiveHandling<Shape>) {
switch (s.kind) {
case 'circle':
break;
// error here: `s` is narrowed to `RequireExhaustiveHandling<WithKnownCases<Rectangle, UnknownShape>>
// TS complains that case Rectangle is not handled
}
};
|
Brain dump on some parts of the logic that would have to be worked out here: For
With the constrained version (
|
I wonder if the idea I proposed here could be the basis for implementing "open-ended unions" more generally, on top of
|
Suggestion
Ability to discriminate between union members when not all union members are known.
Use Cases
Examples
Checklist
My suggestion meets these guidelines:
Workarounds
Cast the general type to a union of known type.
The text was updated successfully, but these errors were encountered: