GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Suggestion: treat `in` operator as type guard which asserts property existence · Issue #21732 · microsoft/TypeScript

Suggestion: treat in operator as type guard which asserts property existence #21732

New issue
Copy link
New issue
Copy link
Closed
#50666
Closed
Suggestion: treat in operator as type guard which asserts property existence#21732
#50666
Copy link
Labels
CommittedThe team has roadmapped this issueThe team has roadmapped this issueEffort: ModerateRequires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual".Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual".Fix AvailableA PR has been opened for this issueA PR has been opened for this issueSuggestionAn idea for TypeScriptAn idea for TypeScript
Milestone
Backlog

Description

@jcalz
Joe Calzaretta (jcalz)
opened on Feb 7, 2018
Issue body actions

TypeScript Version: 2.8.0-dev.20180204

Search Terms: in operator type guard generic assert

Code

function f<K extends string, T extends object>(key: K, genericObj: T, concreteObj: {foo: string}) {
  if ('a' in concreteObj) {
    concreteObj.a // error, Property 'a' does not exist on type 'never'.
  }
  if ('a' in genericObj) {
    genericObj.a // error, Property 'a' does not exist on type 'T'.
  }
  if (key in concreteObj) {
    concreteObj[key]; // error, Type 'K' cannot be used to index type '{ foo: string; }'
  }
  if (key in genericObj) {
    genericObj[key] // error, Type 'K' cannot be used to index type 'T'.
  }
}

Actual behavior:
The compiler does not recognize that the objects have relevant keys even after checking for the existence of the key with the in operator. According to a comment by Nathan Shively-Sanders (@sandersn), the in type guard (as implemented in #15256) narrows by eliminating members from a union; it does not assert that a key exists.

Desired behavior:
The compiler would assert that each object had a property with the relevant key, after having checked for the existence of the key with the in operator. Note that one possible implementation of an asserting type guard would look like

function inOperator<K extends string, T extends object>(k: K, o: T): o is T & Record<K, unknown> {
  return k in o;
}

but this does not behave exactly as desired, possibly due to the bug in #18538: (not sure if #18538 was officially fixed, but it's not erroring anymore)

function g<K extends string, T extends object>(key: K, genericObj: T, concreteObj: { foo: string }) {
  if (inOperator('a', concreteObj)) {
    concreteObj.a // okay
  }
  if (inOperator('a', genericObj)) {
    genericObj.a // okay
  }
  if (inOperator(key, concreteObj)) {
    concreteObj[key]; // okay
  }
  if (inOperator(key, genericObj)) {
    genericObj[key] // okay
  }
}

If a fix for #18538 appears and makes the g() function compile without error, great. Otherwise, maybe the property assertion for in could happen some other way. Not sure.

Playground Link: Here

Related Issues:
#10485, Treat in operator as type guard
#15256, Add type guard for in keyword
#18538, Error when mixing keyof and intersection type and type variable (fixed?)

(EDIT: #18538 seems to be fixed)
(EDIT: change any to unknown)

Reacted by Ankur Oberoi, Greg Ros, SlurpTheo, Olaf Kasper, Bo Lingen, Thomas den Hollander, Aaron Beall, Donald Pipowitch, Matt Kantor, interphx and 301 more
Reacted by Chris, Juan Pablo de la Torre, vatosarmat, Luiz Felipe Frazão, Joshua Baker, Kasper Peulen, Sebastian Malton, queltos, Torleif Berger, Dmytro Parzhytskyi and 32 more
Reacted by Torleif Berger, a11delavar, Jonas, Alfonso Andrés López Molina, Luka Samkharadze, L-Sun, Albert Mañosa, ayame113 (val440), Szymon Marczak, Lucas Genzelis and 12 more

Activity

Next
  1. mhegazy commented on Feb 7, 2018

    @mhegazy
    Mohamed Hegazy (mhegazy)
    on Feb 7, 2018
    Contributor
    More actions

    related discussion also in #10715 about type guards "evolving" the type of an expression.

  2. Mohamed Hegazy (mhegazy)
    added
    SuggestionAn idea for TypeScriptAn idea for TypeScript
    on Feb 7, 2018
  3. GregRos commented on Feb 12, 2018

    @GregRos
    Greg Ros (GregRos)
    on Feb 12, 2018
    More actions

    This! Currently, we have the odd situation that when you write:

    if ("assign" in Object) {
        Object.assign(target, ...args);
    }
    

    When the ES6 typings aren't loaded, it will narrow Object down to never in the if statement!

  4. mhegazy commented on Feb 12, 2018

    @mhegazy
    Mohamed Hegazy (mhegazy)
    on Feb 12, 2018
    Contributor
    More actions

    Greg Ros (@GregRos) please see #21517

  5. GregRos commented on Feb 14, 2018

    @GregRos
    Greg Ros (GregRos)
    on Feb 14, 2018
    More actions

    Mohamed Hegazy (@mhegazy) Yup, I know. I was giving it as an example for what this suggestion would fix. I know why it's happening.

  6. mattmccutchen commented on Jul 17, 2018

    @mattmccutchen
    Matt McCutchen (mattmccutchen)
    on Jul 17, 2018
    Contributor
    More actions

    Let's add some other ways of asserting property existence from #25720 to this proposal:

    let x: unknown;
    
    // All these should narrow x to {prop: unknown} (s/any/unknown/ from original proposal by Matt)
    "prop" in x;
    x.prop != null;
    x.prop !== undefined;
    typeof x.prop !== "undefined";
    
    // typeof should work on properties of the unknown variable
    typeof x.prop === "string"; // should narrow x to {prop: string}
    Reacted by Alec Larson, interphx, Matt Kantor, Micah Zoltu, Denis Sokolov, Sander Mol, Matt, Paul Merrill, Joe Barnett, Joshua Baker and 40 more
  7. mhegazy commented on Jul 27, 2018

    @mhegazy
    Mohamed Hegazy (mhegazy)
    on Jul 27, 2018
    Contributor
    More actions

    Similar requests in #10715, #25720, and #25172

  8. leighman commented on Aug 9, 2018

    @leighman
    Jack Leigh (leighman)
    on Aug 9, 2018
    More actions

    Matt McCutchen (@mattmccutchen) I think you'd want those to narrow to {prop: unknown} rather than {prop: any} wouldn't you?

    Reacted by Felipe, James Bromwell, Hen Greville, zakcodez, Dennis Schridde and Mitch Ryan
  9. felipeochoa commented on Aug 12, 2018

    @felipeochoa
    Felipe (felipeochoa)
    on Aug 12, 2018
    More actions

    Matt McCutchen (@mattmccutchen) The problem is that trying to access a property on null/undefined will throw an error at runtime. I would support adding those type guards, but only on the object type. (If there were a way to get an unknownWithoutNullOrUndefined type, that would be even better)

    Reacted by Joe Calzaretta, Joshua Baker, Luke Page, Matthew Robb and Aaron Cowie
  10. RyanCavanaugh commented on Aug 13, 2018

    @RyanCavanaugh
    Ryan Cavanaugh (RyanCavanaugh)
    on Aug 13, 2018
    Member
    More actions

    Joe Calzaretta (@jcalz) can you highlight the differences between #10485 and what you'd want to happen?

  11. jcalz commented on Aug 14, 2018

    @jcalz
    Joe Calzaretta (jcalz)
    on Aug 14, 2018
    ContributorAuthor
    More actions

    TL;DR: #10485 narrows by filtering union constituents. This suggestion narrows by adding properties to the type.

    In cases where y is not a union type or none of the constituents have an explicit property named x, the test if (x in y) should not try to filter union constituents, but should instead narrow y by intersecting its type with Record<typeof x, unknown>.


    In #10485, the type of the object you're checking with in is meant to be a union, and the type guard filters that union based on whether the constituents do or do not explicitly have a property of the relevant name:

    type A = { w: string, x: number };
    type B = { y: string, z: number };
    function foo(p: A | B): number {
      if ('w' in p) {
        return p.x; // p is narrowed to A
      } else {
        return p.z; // p is narrowed to B
      }
    }

    Note that this isn't exactly sound, since you can call

    foo({ y: "oops", z: 100, w: true }); 

    but the point of property checking on unions is usually to use that property as a discriminant, and the sound behavior would probably annoy the heck out of developers.


    Compare to the following:

    function bar(p: {}): string {
      if ('w' in p) {
        return String(p.w); // error?! ☹
      } else {
        return "nope";
      }
    }

    It is surprising that after what feels like an explicit test for p.w, the compiler still doesn't know that p.w exists. Worse, p is narrowed to never, probably because of #10485.

    What I want to see happen is:

    • Only do the narrowing in Treat in operator as type guard #10485 if the type is a union where at least one constituent explicitly features the relevant property (edit: optional properties count as "explicit" also).
    • Otherwise (or afterwards), have the x in y check be equivalent to the type guard y is typeof y & Record<typeof x, unknown> (modulo the bug in For object type C, type parameter T and mapped type { [K in keyof T & C]: any }, keyof C isn't assignable to K #18538).
    Reacted by Ryan Cavanaugh, SlurpTheo, Matt Kantor, Sean Vieira, Jeremy Scheff, Brad Zacher, pierre, Luke Page, Peter Flynn, Gabriele Tomberli and 5 more
  12. Ryan Cavanaugh (RyanCavanaugh)
    added
    In DiscussionNot yet reached consensusNot yet reached consensus
    on Aug 14, 2018
  13. sirian commented on Aug 19, 2018

    @sirian
    sirian
    on Aug 19, 2018
    Contributor
    More actions

    Joe Calzaretta (@jcalz) This guard doesn't work with partial interfaces

    function foo(x: {foo: "bar"} | {toFixed?: () => any}) {
      if (inOperator("toFixed", x)) {
        x.toFixed(); // Error, Object is of type unknown
      }
    }
    Reacted by Michael Kriese
  14. jcalz commented on Aug 19, 2018

    @jcalz
    Joe Calzaretta (jcalz)
    on Aug 19, 2018
    ContributorAuthor
    More actions

    sirian Yeah, strictly speaking, all you know is that x.toFixed exists, since a value of type {foo: "bar"} may contain a toFixed property (e.g.,

    foo(Object.assign({ foo: "bar" as "bar" }, { toFixed: "whoops" }));  // no error

    ), but assuming people still want the current behavior in #10485 where we eliminate {foo: "bar"} from consideration as soon as we find a toFixed property, then this suggestion is to apply the inOperator() behavior after that elimination.

    So in your case, x would first be narrowed to {toFixed?: () => any} as per #10485 and then to {toFixed: (() => any) | undefined} via something like inOperator()... meaning that toFixed is definitely there but it might be undefined (since ? is ambiguous about whether the property is actually missing or present but undefined.)

    If you want a better idea what the behavior would end up being like, try the following complex user-defined type guard using conditional types:

    type Discriminate<U, K> = ( U extends any ? K extends keyof Required<U> ? U : never : never ) 
      extends infer D ? [D] extends [never] ? U : D : never
            
    function hybridInOperator<K extends keyof any, T>(
      k: K, 
      o: T
    ): o is Discriminate<T, K> & Record<K, unknown> {
        return k in o;
    }

    producing:

    function foo(x: { foo: "bar" } | { toFixed?: () => any }) {
        if (hybridInOperator("toFixed", x)) {
          x.toFixed(); // error, possibly undefined
          if (x.toFixed) x.toFixed();   // okay
        }
    }

    Does that seem better?

  15. sirian commented on Aug 20, 2018

    @sirian
    sirian
    on Aug 20, 2018
    Contributor
    More actions

    Joe Calzaretta (@jcalz)
    Better, but there is another problem with type infer. Look at example

    function foo(x: { foo: "bar" } | Number | { toFixed?: () => any }) {
        if (hybridInOperator("toFixed", x)) {
            x.toFixed(); // no error. since x resolved as Number
            if (x.toFixed) x.toFixed();   // okay
        }
    }

    I also tried various type guards. But always found a new counterexample(

    Upd. As a quick fix - if you change o is Discriminate<T, K> & Record<K, unknown> to o is Extract<T, Discriminate<T, K>>. then x will be resolved as Number | { toFixed?: () => any }

    Upd2.

    So in your case, x would first be narrowed to {toFixed?: () => any} as per #10485 and then to {toFixed: (() => any) | undefined}

    Not right... toFixed would be narrowed to unknown... Look at example. So and error in #21732 (comment) screenshot was not "object is possibly undefined"

    image

  16. 94 remaining items

  17. Anders Hejlsberg (ahejlsberg)
    closed this as completedin #50666on Sep 19, 2022
  18. jcalz commented on Sep 19, 2022

    @jcalz
    Joe Calzaretta (jcalz)
    on Sep 19, 2022
    ContributorAuthor
    More actions

    It's happening!!!

    Reacted by Will Slattum, Antoine Moreaux, lazytype, NicoVIII, Trond Bergquist, Jacek Nowacki, Albert Mañosa, Yuriy Burychka, Katie Byers, Artem Baranov and 8 more
  19. DetachHead commented on Feb 9, 2023

    @DetachHead
    DetachHead
    on Feb 9, 2023
    Contributor
    More actions

    #50666 did not fully fix this issue:

    I'm marking this PR as fixing #21732 even though it doesn't address the case of key in obj where key is of some generic type.

    function f<K extends string, T extends object>(key: K, genericObj: T, concreteObj: {foo: string}) {
      if ('a' in concreteObj) {
        concreteObj.a // no longer an error
      }
      if ('a' in genericObj) {
        genericObj.a // no longer an error
      }
      if (key in concreteObj) {
        concreteObj[key]; // still an error
      }
      if (key in genericObj) {
        genericObj[key] // still an error
      }
    }

    playground

    can this issue be re-opened? or is there a new issue for this?

    Reacted by Sebastian Malton, Marcus Riemer, Leandro Aguiar, Albert Mañosa, Bernhard Häussner, Katie Byers, Eugene, btoo, Alexander, Nathan McWilliams and 2 more
  20. lazytype commented on Feb 9, 2023

    @lazytype
    lazytype
    on Feb 9, 2023
    More actions

    DetachHead How would you expect this to work?

      // `concreteObj` is type `{foo: string}`
      if (key in concreteObj) {
        // `concreteObj` is narrowed to ???
        concreteObj[key];
      }

    concreteObj being type {foo: string} doesn't mean that 'foo' is the only key in concreteObj. So let's say in the conditional we narrowed the type of concreteObj down to {foo: string} & Record<K, unknown>. Now there'd be no type error, but it would allow code like this:

    function f<K extends string, T extends object>(key1: K, key2: K, genericObj: T, concreteObj: {foo: string}) {
      if (key1 in concreteObj) {
        concreteObj[key2];
      }

    This is obviously unsafe, and there's no way in the type system to express that concreteObj has key1 but not key2.

    Reacted by DetachHead, Bernhard Häussner, Jérémy Rialland and Leon Adler
    Reacted by Artur Klesun
    Reacted by SlurpTheo
  21. ehoogeveen-medweb commented on Feb 9, 2023

    @ehoogeveen-medweb
    Emanuel Hoogeveen (ehoogeveen-medweb)
    on Feb 9, 2023
    More actions

    Yeah, you'd need a construct like valueof key1 (so that in the 4th case, valueof key1 extends keyof genericObj). Not sure where excess properties would fit into that, though. Either way, noUncheckedIndexedAccess is very difficult to use without something like that (and without something to represent the inferred minimum length of arrays).

    Reacted by DetachHead and Bernhard Häussner
  22. bxt commented on Feb 15, 2023

    @bxt
    Bernhard Häussner (bxt)
    on Feb 15, 2023
    More actions

    I think it does not make sense that TypeScript assumes there are additional properties somehow on an object but also does not allow adding new properties to an object at the same time. Just like an if narrows down a type, adding properties to an object could narrow down the type to objects that also include this property. Consider this example:

    const objectWithKnownKeys = {
        a: 1,
        b: 2,
    } as const;
    
    // This is an error but maybe it should work and just change
    // the type of objectWithKnownKeys to include {c: 3} maybe?
    objectWithKnownKeys.c = 3;
    
    const key: string = 'a';
    
    if (key in objectWithKnownKeys) {
        // TypeScript should know here there are no additional keys or allow setting c
        const value = objectWithKnownKeys[key];
    }
    

    So maybe the actual bug is that objectWithKnownKeys.c = generates an error? Basically TS2339 should not go off when setting. I can find an old issue #766 about this but related to classes. I would also allow to construct object like this instead of relying on any or Partial<>:

    type SomeObject = { a: number, b?: number }
    // We have to rely on "any", even Partial<SomeObject> would cause an error later
    const initiallyEmptyObject: any = {};
    initiallyEmptyObject.a = 1;
    if (Math.random() > 0.5) { // or some logic
        initiallyEmptyObject.b = 2;
    }
    const someObject: SomeObject = initiallyEmptyObject;
    

    But I assume this would be a pretty big change and there would need to be a solution for letting people know when they accidentally create a new property with a typo, so that's your reason why this will probably never be fixed.

  23. RyanCavanaugh commented on Feb 15, 2023

    @RyanCavanaugh
    Ryan Cavanaugh (RyanCavanaugh)
    on Feb 15, 2023
    Member
    More actions

    there would need to be a solution for letting people know when they accidentally create a new property with a typo

    This is the entire crux of it; a computer can't tell whether this code is intentional or not:

    const dimensions = { width: 0, height: 0 };
    dimensions.widht = 3;

    Our experience is that detecting retrospectively-obvious-to-human typos is seen as worth the false positives in other cases. It's why we have type annotations.

    Reacted by James Bromwell, DetachHead, Albert Mañosa, Leon Adler and SlurpTheo
  24. aaroncowie commented on Feb 17, 2023

    @aaroncowie
    Aaron Cowie (aaroncowie)
    on Feb 17, 2023
    More actions

    there would need to be a solution for letting people know when they accidentally create a new property with a typo

    This is the entire crux of it; a computer can't tell whether this code is intentional or not:

    const dimensions = { width: 0, height: 0 };
    dimensions.widht = 3;

    Our experience is that detecting retrospectively-obvious-to-human typos is seen as worth the false positives in other cases. It's why we have type annotations.

    Could "as const" be used to detect that case?

    const dimensions = { width: 0, height: 0 } as const;
    dimensions.widht = 3;
  25. DetachHead commented on Feb 17, 2023

    @DetachHead
    DetachHead
    on Feb 17, 2023
    Contributor
    More actions

    if you want to allow adding unknown keys to an object, you can define it like so in the type:

    interface Dimensions {
        width: number
        height: number
        [key: string]: number
    }
    const dimensions: Dimensions = { width: 0, height: 0 };
    dimensions.widht = 3; // no error

    that's an edge case though, and as Ryan Cavanaugh (@RyanCavanaugh) said it's far more likely that assigning an unknown key is an error, which is why it wouldn't make sense to not show an error by default, expecting the user to use as const to opt into the functionality they'd want 99% of the time

    Reacted by Marcus Riemer and Artur Klesun
  26. KotlinIsland commented on Feb 17, 2023

    @KotlinIsland
    KotlinIsland
    on Feb 17, 2023
    More actions

    Aaron Cowie (@aaroncowie) A subtype could already contain that key with an unrelated type, therefor losing type soundness.

    function foo(a: { width: number, height: number }) {
        a.length = "idk"
    }
    const a = { width: 0, height: 0, length: 0 }
    foo(a)
    a.length // STRING????? HUUUHHH?????
  27. SlurpTheo commented on Dec 11, 2023

    @SlurpTheo
    SlurpTheo
    on Dec 11, 2023
    More actions

    lazytype

    function f<K extends string, T extends object>(key1: K, key2: K, genericObj: T, concreteObj: {foo: string}) {
      if (key1 in concreteObj) {
        concreteObj[key2]; // ts(2536): Type 'K' cannot be used to index type '{ foo: string }'.
      }

    This is obviously unsafe, and there's no way in the type system to express that concreteObj has key1 but not key2.

    I'm not following what's obvious about this issue/thread/example. It makes sense to me that you can't index by key2 as f<"foo" | "BAR", { foo: string }>("foo", "BAR", { foo: "a3" }, { foo: "a4" }) is a type-valid caller of this API.

  28. lazytype commented on Dec 11, 2023

    @lazytype
    lazytype
    on Dec 11, 2023
    More actions

    It makes sense to me that you can't index by key2

    The goal of the comment I was replying to is to be able to index concreteObj by key1 after checking for key1's existence with the in operator. What is "obvious" is that it would be unsafe to index concreteObj by key2 if only checking for the existence of key1. My point is that from the perspective of the type system key1 and key2 are of the same type. So there's no way to model the behavior that concreteObj is indexable by key1 but not key2 in the current type system.

  29. github-merge-queue
    added a commit that references this issue on Jan 22, 2024

    [PLAY-1083] Linter implementation: Kits F - J (#3021)

    f53e0c0
  30. klesun commented on Jul 24, 2025

    @klesun
    Artur Klesun (klesun)
    on Jul 24, 2025
    More actions

    lazytype, I believe you are looking at it from the wrong angle. The in check should not change the type of the concreteObj, it should change the type of the key - I believe that's what DetachHead meant when he asked if this issue can be re-opened since it was not fully fixed which is my big request also.

    type InputKey = "goodKey1" | "goodKey2" | "badKey"; 
    
    const obj = {
        "goodKey1": 123,
        "goodKey2": 234,
    };
    
    const inputKeyOptions: InputKey[] = ["goodKey1", "goodKey2", "badKey"];
    const inputKey: InputKey = inputKeyOptions[Math.floor(Math.random() * 3)];
    
    if (inputKey in obj) {
        // gives an error, but compiler should have narrowed the type of inputKey to "goodKey1" | "goodKey2"
        const value = obj[inputKey];
    }
  31. lazytype commented on Jul 24, 2025

    @lazytype
    lazytype
    on Jul 24, 2025
    More actions

    Artur Klesun (@klesun) you did not read my comment carefully enough. You cannot narrow inputKey to "goodKey1" | "goodKey2" because from the typechecker's perspective obj is of type { goodKey1: number; goodKey2: number } which means any object which has "goodKey1" and "goodKey2" keys with numerical values. That includes objects that can have additional keys. The typechecker does not know that obj has exactly those keys. This concept doesn't exist in TypeScript.

    Reacted by snarbles2
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

    CommittedThe team has roadmapped this issueThe team has roadmapped this issueEffort: ModerateRequires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual".Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual".Fix AvailableA PR has been opened for this issueA PR has been opened for this issueSuggestionAn idea for TypeScriptAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      • BacklogNo due date

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.