Omitted property does not narrow discriminated union contextual type #41759
Description
Activity
In this case, I think this'd be fixable by adjusting
discriminateContextualTypeByObjectMembersanddiscriminateContextualTypeByJSXAttributesto includeundefined'able discriminant props which don't appear in thenode's properties. (They both currently filter the props which do exist on the input object down to the discriminant ones right now, so the possiblyundefineddiscriminants from the contextual type would need to be appended to those)Reacted by Andrew Branch and Ryan CavanaughRyanCavanaugh commented
on Dec 1, 2020 MemberMore actionsTagging backlog since this is a longstanding shortcoming, but we can take it earlier if someone's eager to take a stab.
Reacted by Andrew Branch, Wes van Vugt, Erik Brinkman and Ryan WersalThis is same issue as this one right?
type Props = { mode?: boolean; nodes?: string; } type B = | { enabled?: false; } | { enabled: true; foo: string; }; // no error UNEXPECTED ❌ export const a: Props & B = { mode: true, nodes: ",", foo: "", }; type B2 = | { enabled: false; } | { enabled: true; foo: string; }; // compiles as expected ✅ export const a2: Props & B2 = { mode: true, nodes: ",", enabled: true, foo: "", }; // err as expected ✅ export const a3: Props & B2 = { mode: true, nodes: ",", foo: "", }; // err as expected ✅ export const a4: Props & B2 = { mode: true, nodes: ",", enabled: false, foo: "", };
Output
// no error UNEXPECTED ❌ export const a = { mode: true, nodes: ",", foo: "", }; // compiles as expected ✅ export const a2 = { mode: true, nodes: ",", enabled: true, foo: "", }; // err as expected ✅ export const a3 = { mode: true, nodes: ",", foo: "", }; // err as expected ✅ export const a4 = { mode: true, nodes: ",", enabled: false, foo: "", };
Compiler Options
{ "compilerOptions": { "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictPropertyInitialization": true, "strictBindCallApply": true, "noImplicitThis": true, "noImplicitReturns": true, "alwaysStrict": true, "esModuleInterop": true, "declaration": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "moduleResolution": 2, "target": "ES2017", "jsx": "React", "module": "ESNext" } }Playground Link: Provided
Sort of a "solution" is to do this:
// Solution is to specify all props from the non optional `enabled` as optional undefined fields type B3 = | { enabled?: false; foo?: undefined} | { enabled: true; foo: string; }; // err as expected ✅ export const a5: Props & B3 = { mode: true, nodes: ",", };
but it's not very nice. (I came across this when I was trying to use discriminated unions for react prop types)
- added a commit that references this issue
on Apr 24, 2021 - added a commit that references this issue
on Mar 4, 2022 Andarist commented
on Apr 28, 2022 ContributorMore actionsAndrew Branch (@andrewbranch) Is this supposed to be still open? This doesn't error in 4.6, see playground
andrewbranch commented
on Apr 28, 2022 MemberAuthorMore actionsHuh, the fix was reverted, so I’m not sure what happened.
- addedHas ReproThis issue has compiler-backed repros: https://aka.ms/ts-reprosThis issue has compiler-backed repros: https://aka.ms/ts-repros
on Apr 28, 2022 typescript-bot commented
on Apr 28, 2022 ContributorMore actions👋 Hi, I'm the Repro bot. I can help narrow down and track compiler bugs across releases! This comment reflects the current state of the repro in the issue body running against the nightly TypeScript.
Issue body code block by Andrew Branch (@andrewbranch)
❌ Failed: -
Argument of type '{ cb: (n: string) => number; }' is not assignable to parameter of type 'DiscriminatorTrue | DiscriminatorFalse'. Type '{ cb: (n: string) => number; }' is not assignable to type 'DiscriminatorFalse'. Types of property 'cb' are incompatible. Type '(n: string) => number' is not assignable to type '(x: number) => void'. Types of parameters 'n' and 'x' are incompatible. Type 'number' is not assignable to type 'string'.
Historical Information
Version Reproduction Outputs 4.3.2, 4.4.2, 4.5.2, 4.6.2 ❌ Failed: -
Argument of type '{ cb: (n: string) => number; }' is not assignable to parameter of type 'DiscriminatorTrue | DiscriminatorFalse'. Type '{ cb: (n: string) => number; }' is not assignable to type 'DiscriminatorFalse'. Types of property 'cb' are incompatible. Type '(n: string) => number' is not assignable to type '(x: number) => void'. Types of parameters 'n' and 'x' are incompatible. Type 'number' is not assignable to type 'string'.
4.2.2 ❌ Failed: -
Parameter 'n' implicitly has an 'any' type.Argument of type '{ cb: (n: string) => number; }' is not assignable to parameter of type 'DiscriminatorTrue | DiscriminatorFalse'. Type '{ cb: (n: string) => number; }' is not assignable to type 'DiscriminatorFalse'. Types of property 'cb' are incompatible. Type '(n: string) => number' is not assignable to type '(x: number) => void'. Types of parameters 'n' and 'x' are incompatible. Type 'number' is not assignable to type 'string'.
andrewbranch commented
on Apr 28, 2022 MemberAuthorMore actionsAnyone know the state of this? Is it worth a new issue? Looks to exist in 4.9.5
The OP's issue is still fixed, so if you have something that seems similar, definitely file a new bug.
- locked as resolved and limited conversation to collaborators
on Oct 21, 2025
TypeScript Version: 4.2.1
Search Terms: discriminated union optional property
Code
Expected behavior:
No implicit any error on the third call site
Actual behavior:
Implicit any error on the callback parameter at the third call site
Playground Link
Related Issues: #31404 (comment) is this issue, but the OP there is different.
Notes:
This pattern sometimes appears in React component props, where convention is to make boolean properties optional and only pass them as
true(usually with the shorthand<MyComponent boolProp />). I actually suggested using discriminated union props for React components in an old blog post, and noted this issue in a footnote, calling it a possible bug, but didn’t file it at the time because I had low confidence it wasn’t a duplicate or design limitation. It was later mentioned in #31404 (comment), but was probably ignored because it was assumed to be an instance of the OP’s issue, which was determined to be a design limitation. I dove into this again because someone tweeted at me asking about that footnote after reading the post.