Skip to content

Narrow "string" to "keyof T" with "in" guard? #12892

Closed

Description

@danvk

Should if (x in t) {} narrow the type of x to be keyof T inside the conditional block?

TypeScript Version: 2.1.4

Code

interface Spec<T> {
  description: string;
  default: T;
}
function getDescription<T>(specs: {[k in keyof T]: Spec<T[k]>}, k: string) {
  if (k in specs) {
    return specs[k].description;
    // error TS2536: Type 'string' cannot be used to index type '{ [k in keyof T]: Spec<T[k]>; }'.
  }
  return null;
}

Expected behavior:

This should type check. k should be narrowed from string to keyof T inside the if (k in specs) guard.

Actual behavior:

The type of k remains string inside the guard, leading to the indexing error.

Activity

  1. zpdDG4gta8XKpMCd commented on Dec 13, 2016

    @zpdDG4gta8XKpMCd

    yes it should as it was proven at runtime to be true

    (maybe not keyof T itself, but something that extends keyof T)

  2. DanielRosenwasser commented on Dec 14, 2016

    @DanielRosenwasser
    Member

    Aleksey-Bykov It is actually not something that extends keyof T; it's the opposite, and keyof T is the lower bound. Adding more elements to the union will make it a supertype (i.e. a less-specific type).

  3. zpdDG4gta8XKpMCd commented on Dec 14, 2016

    @zpdDG4gta8XKpMCd

    true, i always forget that unions are the other way around

  4. mhegazy commented on Jan 23, 2017

    @mhegazy
    Contributor

    You can capture the type of specs as a generic type, and constraint k to be keyof that. if you still want to drill in and get a property type you can use lookup types, e.g.:

    export interface Map<T> {
      [x: string]: T
    }
    
    interface Spec<T> {
      description: string;
      default: T;
    }
    
    function getDescription<S extends Map<Spec<any>>, K extends keyof S>(specs: S, k: K) {
      if (k in specs) {
        return specs[k].description;
        // error TS2536: Type 'string' cannot be used to index type '{ [k in keyof T]: Spec<T[k]>; }'.
      }
      return null;
    }

    For getting the type of default, for example, you can write it as such:

    function getDefault<S extends Map<Spec<any>>, K extends keyof S>(specs: S, k: K): S[K]["default"] {
      if (k in specs) {
        return specs[k]["default"];
        // error TS2536: Type 'string' cannot be used to index type '{ [k in keyof T]: Spec<T[k]>; }'.
      }
      return null;
    }
    
    getDefault({ x: { description: "s", default: 1 } ,y: { description: "s", default: "ss" } }, "x"); // number
    getDefault({ x: { description: "s", default: 1 } ,y: { description: "s", default: "ss" } }, "y"); // string
  5. mhegazy commented on Jan 23, 2017

    @mhegazy
    Contributor

    The reason why narrowing is not really correct is S can have more properties at runtime than we know about at design time. so if we narrow k to keyof specs it would be more restrictive than users intend.

  6. danvk commented on Jan 24, 2017

    @danvk
    ContributorAuthor

    Thanks for the explanation, Mohamed Hegazy (@mhegazy)

  7. locked and limited conversation to collaborators on Jun 19, 2018
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