镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Cannot access property that exists only on some union members #36194

Closed

Description

@AlCalzone

TypeScript Version: Nightly

Search Terms: Union property does not exist

Code

interface SomeOptions {
    foo: boolean;
}

interface OtherOptions {
    bar: boolean;
    baz?: number;
}

function doSomething(options: SomeOptions | OtherOptions) {
    // Error:
    const baz1 = options.baz ?? 1;
    // Workaround:
    const baz2 = ("baz" in options ? options.baz : undefined) ?? 1;
}

Expected behavior:
I can access options.baz and its type is number | undefined.

Actual behavior:
Error: property baz does not exist on type SomeOptions. The workaround does work, but is unnecessarily verbose and does unnecessary runtime checks.

I know this is mentioned in the handbook, but I'd like to advocate for this behavior to be reconsidered. It was mentioned that the type at runtime might be different than expected, but this is also true for the workaround.

I also know I could add all these properties to the other union members (baz?: never), but this is quite tedious and error prone for complex real-world types.

Playground Link: https://www.typescriptlang.org/play/?ts=3.8.0-dev.20200114&ssl=1&ssc=1&pln=15&pc=2#code/JYOwLgpgTgZghgYwgAgMoHsC2EDyAHMYdEAZ2QG8AoZG5GddALmQCMGAbCOEAbkoF9KlUJFiIUOMAAto+QsTJVarOFGZt0nbn2Us4ALwD8zEAFdMLaH0GUYpkAnkhkAE3QZs00AHMAFOgIiUmYPXECFZAAfZEkZKDkgkgBKCmpaAHp05ABRKCh0NTSaBAUwFX0ARmQAXmQApxIAOj19ZENDZAqdDKyAdQKAaxJGIuQS0jKWgCYa5F8AIhb55FA68NI2tYbmg2RmexcIGFAIFxT2zusgA

Related Issues:

Activity

  1. changed the title [-]Cannot access property that exists on some union members[/-] [+]Cannot access property that exists only on some union members[/+] on Jan 15, 2020
  2. jcalz commented on Jan 15, 2020

    @jcalz
    Contributor

    Related to the declined #33736.

  3. AlCalzone commented on Jan 15, 2020

    @AlCalzone
    ContributorAuthor

    Joe Calzaretta (@jcalz) The OP there is actually quite similar - but the issue has been closed because the control flow narrowing effects are undesired. My intent here is just to get the non-chained property access.

    I've read in other issues that this behavior requires exact types (so we cannot pass a SomeOptions & {baz: boolean} for example). In that case, please put this request on the wishlist for that feature.

  4. RyanCavanaugh commented on Jan 15, 2020

    @RyanCavanaugh
    Member

    This has been the behavior for about five years now; #1260 would be the issue to follow if you expect changes.

  5. AlCalzone commented on Jan 15, 2020

    @AlCalzone
    ContributorAuthor

    Ryan Cavanaugh (@RyanCavanaugh) is it realistic to expect such a change (for example in combination with exact types) at some point in the future?

  6. RyanCavanaugh commented on Jan 16, 2020

    @RyanCavanaugh
    Member

    We'd have to think about it. Just hypothesizing, let's say you had

    type A = exact { x: number, y: number };
    type B = exact { a: string, b: string };
    declare const ab: A | B;

    What would be the ideal rules here?

    • Allowing ab.a in all positions would be kind of bonkers. It would imply you could write pure nonsense like ab.a?.toLowerCase() + ab.x?.toFixed()
    • So maybe you say that ab.a is only allowable sometimes. But where?
      • Restricting it to "inside conditionals" is just an immediate "Bug: TypeScript is iNcOnSiStEnT about where I can read this property" bug farm; there are also positions which are ambiguously conditional or not (e.g. left side of | / || / & / &&)
      • Restricting it to a more "obvious intent" form "a" in ab is what we have today
      • But maybe there's something that would make sense?
    • (Something else?)
  7. AlCalzone commented on Jan 16, 2020

    @AlCalzone
    ContributorAuthor

    I have no good answer for that. The example you posted is clearly much more complicated than my simple use case of a single property access.

  8. typescript-bot commented on Jan 18, 2020

    @typescript-bot
    Contributor

    This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  9. patrickjcurran commented on Apr 19, 2023

    @patrickjcurran

    Allowing ab.a in all positions would be kind of bonkers. It would imply you could write pure nonsense like ab.a?.toLowerCase() + ab.x?.toFixed()

    I disagree that that is bonkers. The question mark makes it clear that either the left or right term might be undefined. Yes, we are loosing the fact that precisely one of them might be undefined, but that seems like a fair tradeoff.

    For example if you allow access to anything defined in the union, but as optional properties:

    type A = { x: number, y: number };
    type B = { a: string, b: string };
    declare const ab: A | B;
    type LaxAB = Partial<A> & Partial<B>
    const laxAb: LaxAB = ab;
    
    laxAb.a?.toLowerCase() + laxAb.x?.toFixed()
    

    You will still get an error Object is possibly 'undefined'. because the left and right terms might be undefined.

  10. AlCalzone commented on Apr 19, 2023

    @AlCalzone
    ContributorAuthor

    Patrick Curran (@patrickjcurran) the error is because you're trying to concatenate possibly undefined values:

    type A = { x: number, y: number };
    type B = { a: string, b: string };
    declare const ab: A | B;
    type LaxAB = Partial<A> & Partial<B>
    const laxAb: LaxAB = ab;
    
    const test1 = laxAb.a?.toLowerCase();
    const test2 = laxAb.x?.toFixed()
    
    test1 + test2; // Error here!
  11. patrickjcurran commented on Apr 19, 2023

    @patrickjcurran

    AlCalzone understood, my point was that it is good that the type checker doesn't allow that. I'll edit my post to make that more clear. Thank you.

  12. rrain0 commented on Mar 3, 2025

    @rrain0

    It is not an ultimate solution, but i've created a workaround that can cover some cases. Hope this helps someone:

    // Workaround type
    type ObjectUnionOptionalPropsFix<O1 extends object, O2 extends object> =
      | O1 & {
        [OptKeys in keyof Omit<O2, keyof O1>]: undefined
      }
      | O2 & {
        [OptKeys in keyof Omit<O1, keyof O2>]: undefined
      }
    
    interface SomeOptions {
      foo: boolean;
    }
    
    interface OtherOptions {
      bar: boolean;
      baz?: number;
    }
    
    function doSomething(options: ObjectUnionOptionalPropsFix<SomeOptions, OtherOptions>) {
      // Correct✅: options.baz?: number | undefined
      const baz1 = options.baz ?? 1;
    }

    And my case with optional props of React component:

    // Workaround type
    type ObjectUnionOptionalPropsFix<O1 extends object, O2 extends object> =
      | O1 & {
      [OptKeys in keyof Omit<O2, keyof O1>]: undefined
    }
      | O2 & {
      [OptKeys in keyof Omit<O1, keyof O2>]: undefined
    }
    
    type Type = {
      type: 'type1' | 'type2'
    }
    type Category = {
      category: 'category1' | 'category2'
    }
    type TypeOrCategory = Type | Category
    
    
    
    // React component
    const RenderTypeOrCategory = (props: TypeOrCategory) => {
      // Without workaround❌: TS2339: Property type does not exist on type TypeOrCategory
      // Without workaround❌: TS2339: Property category does not exist on type TypeOrCategory
      // With workaround✅: type: 'type1' | 'type2' | undefined
      // With workaround✅: category: 'category1' | 'category2' | undefined
      const { type, category } = props as ObjectUnionOptionalPropsFix<Type, Category>
      
      if (type) return <>
        {/* Render type via type-consuming component */}
        {/* Correct✅: type: 'type1' | 'type2' */}
        {type}
      </>
      return <>
        {/* Render category via category-consuming component */}
        {/* Correct✅: category: 'category1' | 'category2' */}
        {category}
      </>
    }
  13. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions