-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Discriminated unions do not work with never
/ optional discriminant
#38144
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
Comments
Looks like a duplicate of #38024. |
I'm not sure that issue is exactly the same, or it didn't explore far enough. Typescript clearly knows I did try explicitly defining the negative case by defining an interface with
The end result is there is no possible way to represent a case where a property adds on additional |
Because TypeScript has a structural type system. The type of your object literal Normally the excess property checks would prevent you from providing additional properties in your object literal, but that doesn't work due to #38024. https://github.com/microsoft/TypeScript/wiki/FAQ#what-is-structural-typing |
Fair enough, so I need to be able to specify to the type system that having a type property with any value should mismatch against the default case. I had thought I could do that with
But as seen on the playground, this does the opposite and starts demanding a |
An object with an unoptional The proposed mechanism in the OP wouldn't be sound. Consider interface Base {
title: string;
}
interface OtherFoo extends Base {
type: "foo";
foo: string;
}
interface DerivedFoo extends Base {
type: "foo"
foo: number;
} An |
I don't follow how that example relates to the OP. In such a case I would expect foo to be string | number. But that doesn't seem related to the feature I am requesting: The ability to specify a negative case / 'lack-of-property' discriminant. The desired behavior here isn't about duplicate discriminants over a type, but about the ability to use the lack of a discriminant property as a choice in the discriminated types list. Currently it is possible to use discriminant types to select between various consts applied to a type key, I would expect there to also be a method of using the lack-of-key as a discriminant as well. In my use case, W3C DataSchema has several subtypes, each defined by a I can model this fine for property access; the subtype properties only show up when gated with a check for the appropriate type value. The excess property check fails to handle it, however, and protecting against inadvertently bad DataSchema objects is the primary use case of this typing. Edit: Perhaps my original example is too simplified. Here is something that might show the issue better interface TypeIsNumber {
type: "number";
value: number;
}
interface TypeIsString {
type: "string";
value: string;
}
interface DefaultTypeIsBoolean {
type: undefined; // How can we specify the lack of `type` is the discriminant?
value: boolean;
extraProp: string;
}
type MyType = TypeIsNumber | TypeIsString | DefaultTypeIsBoolean;
const asNumber: MyType = {
type: "number";
value: 42 // value should be typed number
// extraProp should not be allowed here.
}
const asString: MyType = {
type: "string";
value: "hello"; // value should be typed string
// extraProp should not be allowed here.
}
const asDefault: MyType = {
value: false // value should be typed boolean
extraProp: "foobar"; // extraProp should be recognized here.
} |
I'm dealing with the same issue in a slightly different use case. I want to narrow the input types of a function based on a discriminant, defaulting to the supertype if the discriminant isn't present; however, I currently have to define the discriminant as In other words, this feels like a buggy, frustrating mess. type Supertype = {}
type Subtype1 = Supertype & { type: 't1' }
type Subtype2 = Supertype & { type: 't2' }
// type Test['requireType'] = 't1' | 't2' | undefined
type Test =
{ requireType?: undefined, eval: (param: Supertype) => void} |
{ requireType: 't1', eval: (param: Subtype1) => void} |
{ requireType: 't2', eval: (param: Subtype2) => void}
// Both of these work fine...
let op1: Test = {
requireType: 't1',
eval: p => // p: Subtype1
}
let op2: Test = {
requireType: 't2',
eval: p => // p: Subtype2
}
// This is a little inconsistent.,.
let op3: Test = {
requireType: undefined,
eval: p => // p: Supertype (inconsistent, sometimes p: any)
}
// And intuitively this should work, but doesn't.
let op4: Test = {
eval: p => // should be p: Supertype ; actually p: any
} |
This basically boils down to a lack of exact types - with structural subtyping, there isn't a way to say "This property can't be present", nor a way to guarantee it doesn't happen through aliasing. |
Unless I misunderstand something, The refinement system doesn't recognize that this the case (which may or may not be for strong technical reasons around implementation), which leads to rejecting valid programs that exploit it. It's a shame because it's a way of closing a hole that the refinement system creates by assuming unspecified fields are not present. Consider, similar to the above:
This is all valid, should be treated as such, and is. Now let's write a function that takes an ABC:
This is a function that says it returns a string, but when passed our If I add All of that said, I'm not sure how the ergonomics ultimately work out of having to sometimes establish that those properties are missing. |
TypeScript Version: 3.8.3
Search Terms:
I am trying to write type specifications for the W3C Thing Definition spec, particularly DataSchema, which is a subset of json-schema.
DataSchema has several subclasses identified by a
type
key, but it can also be used without atype
key, in which case only the base type properties should be allowed.I have been unable to type this in such a way that a unifying type of all sub-interfaces plus base interface blocks the use of derived interface properties when no
type
is specified.Code
Here is a simplified example:
Expected behavior:
There should be an error on
test
, asfoo
is not defined inBase
andDerivedFoo
should not be matched due to notype: "foo"
.Actual behavior:
No error is reported when defining properties for
test
. Intellisense recognizesfoo
despite the lack ofPlayground Link: example
The text was updated successfully, but these errors were encountered: