Skip to content

Pick deep object properties #36783

Closed
Closed
@jcardali

Description

@jcardali

Search Terms

pick, nested, deep

Suggestion

Pick can only be used to access properties at the top level of an object, it should support paths to get at more deeply nested values.

Use Cases

Typing form inputs from GraphQL fragments

Examples

type Company = {
  id: number;
  name: string;
  address: {
    id: number;
    street: string;
    city: string;
    state: string;
  };
}

You could then construct a new type as follows:
type FlatCompany = Pick<Company, "id" | "name" | { "addressId": ["address", "id"] }>

Which would yield:

type FlatCompany = {
  id: number;
  name: string;
  addressId: number;
}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
    This wouldn't change the runtime behavior of existing JavaScript code
    This could be implemented without emitting different JS based on the types of the expressions
    This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
    This feature would agree with the rest of TypeScript's Design Goals.

Activity

jcalz

jcalz commented on Feb 14, 2020

@jcalz
Contributor

It's virtually impossible that the definition of Pick will be modified to do this. See this comment: people rely on the current behavior and performance and implementation of Pick. You are, however, free to introduce into your own code a type alias, which I'll call HybridDeepPick, which behaves in any way you'd like:

type FlatCompany = HybridDeepPick<Company, "id" | "name" | { "addressId": ["address", "id"] }>
/* type FlatCompany = {
    id: number;
    name: string;
    addressId: number;
} */

The main problem I see with writing such a type right now is that using tuples to index arbitrarily far down into a type (see #12290) would require more support for circular types than currently exists (#26980). So anything that is built with today's TypeScript would either need to do something unsupported or have an explicit depth limit, and will be ugly either way.

Here's one ugly way which walks down about six or seven levels at most, does nothing good with types containing index signatures or optional/readonly properties, does not prevent you from using bad nested keys (e.g., ["adress", "streeet"] will silently fail to index anything) and has not been tested thoroughly enough to go anywhere near a production system:

type Tail<T extends readonly any[]> =
  ((...x: T) => void) extends ((h: any, ...r: infer R) => void) ? R : never;

type DeepIdx<T, K extends readonly PropertyKey[]> = K extends [] ? T : K[0] extends keyof T ? DI0<T[K[0]], Tail<K>> : never;
type DI0<T, K extends readonly any[]> = K extends [] ? T : K[0] extends keyof T ? DI1<T[K[0]], Tail<K>> : never;
type DI1<T, K extends readonly any[]> = K extends [] ? T : K[0] extends keyof T ? DI2<T[K[0]], Tail<K>> : never;
type DI2<T, K extends readonly any[]> = K extends [] ? T : K[0] extends keyof T ? DI3<T[K[0]], Tail<K>> : never;
type DI3<T, K extends readonly any[]> = K extends [] ? T : K[0] extends keyof T ? DI4<T[K[0]], Tail<K>> : never;
type DI4<T, K extends readonly any[]> = K extends [] ? T : K[0] extends keyof T ? DI5<T[K[0]], Tail<K>> : never;
type DI5<T, K extends readonly any[]> = K extends [] ? T : K[0] extends keyof T ? DI6<T[K[0]], Tail<K>> : never;
type DI6<T, K extends readonly any[]> = K extends [] ? T : K[0] extends keyof T ? DIX<T[K[0]], Tail<K>> : never;
type DIX<T, K> = never;

type DeepPick<T, K extends Record<keyof K, readonly PropertyKey[]>> = 
  { [P in keyof K]: DeepIdx<T, K[P]> }

type AllKeys<T> = T extends any ? keyof T : never;

type HybridDeepPick<T, K extends (keyof T) | Record<keyof K, readonly PropertyKey[]>> =
  DeepPick<T, { [P in Extract<K, PropertyKey> | AllKeys<Extract<K, object>>]:
    P extends K ? [P] : Extract<K, { [Q in P]: any }>[P]
  }>

So make of that what you will. Good luck!

Playground link to code

RyanCavanaugh

RyanCavanaugh commented on Feb 19, 2020

@RyanCavanaugh
Member

👆 Once we ship commonly-used aliases, touching them in any way at all is guaranteed to produce a huge number of confusing breaks, which is why we've tried to stop doing so except when absolutely necessary.

typescript-bot

typescript-bot commented on Feb 22, 2020

@typescript-bot
Collaborator

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

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

        Participants

        @jcalz@RyanCavanaugh@jcardali@typescript-bot

        Issue actions

          Pick deep object properties · Issue #36783 · microsoft/TypeScript