Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pick deep object properties #36783

Closed
5 tasks done
jcardali opened this issue Feb 13, 2020 · 3 comments
Closed
5 tasks done

Pick deep object properties #36783

jcardali opened this issue Feb 13, 2020 · 3 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@jcardali
Copy link

jcardali commented Feb 13, 2020

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.
@jcalz
Copy link
Contributor

jcalz commented Feb 14, 2020

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 added the Working as Intended The behavior described is the intended behavior; this is not a bug label Feb 19, 2020
@RyanCavanaugh
Copy link
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
Copy link
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
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants