Skip to content

Can't index object in generic function with property name extracted with Pick from derived type #40832

Closed

Description

@popadi

TypeScript Version: 4.1.0-beta

Search Terms:
pick, generic function, keyof

Code

interface IRecord {
  id: number,
  name: string
}

type FilterProperties<T> = Pick<
  T,
  { [K in keyof T]: T[K] extends string ? K : never }[keyof T]
>;

function extract<T>(data: T[], field: keyof FilterProperties<T>): void {
  const record: T = data[0];
  const value: string = record[field]; // see error below
}

const data: IRecord[] = [{ id: 1, name: "one" }, { id: 2, name: "two" }]

extract<IRecord>(data, "id"); // expected: error that id can't be used as a parameter since it's not a string
extract<IRecord>(data, "name") // no error, expected behaviour

Expected behavior:
I'm trying to create a function that receives an object/an array of objects of type T and a field name but the user should be able to call the function only if the type of that field in T is string. FilterProperties is just a type (union) that picks those properties from T and can be easily extended to filter any kind of properties.

Since I was able to call the function correctly on the last line (because name is a field of type string, I was expecting to be able to extract the value in the function too, but I get the error below. Everything works if you define everything explicitly (no generics).

Actual behavior:

Type 'T[{ [K in keyof T]: T[K] extends string ? K : never; }[keyof T]]' is not assignable to type 'string'.
  Type 'T[T[keyof T] extends string ? keyof T : never]' is not assignable to type 'string'.
    Type 'T[keyof T]' is not assignable to type 'string'.
      Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'string'.
        Type 'T[string]' is not assignable to type 'string'.ts(2322)

Playground Link:
https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgJICUIIPZQCbIDeAsAFDLLB4BcyIArgLYBG0ANGRSHIxLQM5gooAOZkAvmTJgAngAcUAMWAAbSFAAKUbAqhhgEfgB4AKgD5kAXmQbgCANZHOyEx3JFkAbQDSlEMnsIGWwYFwBdWhMfMOQIAA9IEDx+ZEFhEBFkAH5kX1oQCAA3aGRxT0Dg0JMwsjMAbilSGHoQBH1sf3ihRDBTMwAKPDgwOEjPMLZkGAMVGgCgkORlNWgtHWh9Qz6ASlpC7CoiZxwQQWQoLFw5kytkIZHPAAYwhvcTs8K4FXo+VKFRW4XHD4TzTCCzF7IAD0UNSEBQ0G0UGQrBU2AA7hJGu8wHdhqM0JhgXhxrdPIRKHMAIyTbi8WgAIg6EAZpUmFKotAATLSeL8GWB0dhWeIaqQyF0oD0jBhLvgBvc4JMGVQGds6tDYYjcMgwAALYaU5AIOAgADkuNYyHo-AgBDgKTgyDkcClvHUqVASEoYDNKRA2FxTrSoglCSlbRlRKuCvxyrpLO2mro2FiUCRk3iCjadpREANhWA2HoUCAA

Let me know if I should update the title to be more descriptive.

Activity

  1. MartinJohns commented on Sep 29, 2020

    @MartinJohns
    Contributor

    You're assuming that T extends string means "T is a string", but that's not the case. It just extends string, but it can be a more narrow literal string type, e.g. 'foo'. Trying to assign 'foo' to string is not allowed.

    edit: It was a long day and this was completely wrong.

  2. RyanCavanaugh commented on Sep 29, 2020

    @RyanCavanaugh
    Member

    Martin Johns (@MartinJohns) that's a valid subtyping

    The problem here is that we don't have the higher-order reasoning to say "If a key came from an operation that filters out keys that aren't strings, then indexing by that key must have produced a string". The FilterProperties type is effectively logically opaque from TS's point of view.

    An alternate way of writing the function that achieves the desired semantics would be:

    interface IRecord {
      id: number,
      name: string
    }
    
    function extract<K extends string>(data: Record<K, string>[], field: K): void {
      const value: string = data[0][field]; // see error below
    }
    
    
    const data: IRecord[] = [{ id: 1, name: "one" }, { id: 2, name: "two" }]
    
    extract(data, "id"); // error that id can't be used as a parameter since it's not a string
    extract(data, "name") // no error, expected behaviour
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

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions