microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.28k stars 12.39k forks source link

Pick<T, "key" as "newKey"> #57503

Closed ijxy closed 6 months ago

ijxy commented 6 months ago

🔍 Search Terms

"remap literal keys"

✅ Viability Checklist

⭐ Suggestion

I'd like to be able to use the as keyword when using Pick:

type T = {
  a: number;
  b: string;
  c: unknown;
  d: boolean;
}

type Wanted = Pick<T, "a" as "hello-a" | "b" as "goodbye-b" | "c">;
// type Wanted = {
//    "hello-a": number;
//    "goodbye-b": string;
//    c: unknown;
// }

📃 Motivating Example

I often find myself remapping fields in third party data and am just getting a bit tired of the very verbose form the definitions for these transformed types usually take, which tends to be combinations of Pick/Omit and intersections.

Perhaps it is just me, but I find these types difficult to grok sometimes--the intersection parts could include types from anywhere, so the complexity jump is potentially enormous.

By contrast, the humble Pick statement is simple and easy to understand--it immediately signifies that whatever the new type is, it does not include anything outside the scope of the original type.

type Wanted<T> = Pick<T, "a" as "hello-a" | "b" as "goodbye-b" | "c">

type Workaround = {
  "hello-a": T["a"];
  "goodbye-b": T["b"];
  c: T["c"];
};

type WorkaroundWithPick = {
  "hello-a": T["a"];
  "goodbye-b": T["b"];
} & Pick<T, "c">;

This is already possible in the context of remapping all keys, such as in:

type Mapped = { [K in keyof T as `hello-${K}`]: T[K] };

💻 Use Cases

  1. What do you want to use this for? Remapping individual keys on a type.

  2. What shortcomings exist with current approaches? Using Pick/Omit + intersections works but is more difficult to understand, because in principal the part in the intersection could introduce types outside the scope of the original.

  3. What workarounds are you using in the meantime? Combining Pick subtypes with manual remapping, see examples.

MartinJohns commented 6 months ago

Pick<> is a userland type, there's no special compiler treatment. How would this proposed syntax work in general? How would it change the definition of Pick<>?

Besides, it's pretty straightforward to create a utility type that helps you, e.g.:

type Data = {
  a: number;
  b: string;
  c: unknown;
  d: boolean;
}

type Projection<T, M extends { [key in keyof T]?: string }, K extends keyof T = never> =
  { [P in K]: T[P] } &
  { [P in keyof M as M[P] & string]: P extends keyof T ? T[P] : never }

type Test1 = Projection<Data, { a: "hello-a", b: "goodbye-b" }, "c">
// { c: unknown; "hello-a": number; "goodbye-b": string }

type Test2 = Projection<Data, { a: "hello-a", b: "goodbye-b" }, "a" | "c">
// { a: number; c: unknown; "hello-a": number; "goodbye-b": string }

type Test3 = Projection<Data, { a: "hello-a", b: "goodbye-b" }>
// { "hello-a": number; "goodbye-b": string }
ijxy commented 6 months ago

@MartinJohns

Pick<> is a userland type, there's no special compiler treatment. How would this proposed syntax work in general? How would it change the definition of Pick<>?

Besides, it's pretty straightforward to create a utility type that helps you, e.g.:

You can get very close to what I want with this:

type pick<T, K extends keyof T | [keyof T, string]> = {
  [P in K as P extends [keyof T, string] ? P[1] : P]: T[P extends [
    keyof T,
    string,
  ]
    ? P[0]
    : P];
};
};

type Test2 = pick<Data, ["a", "hello-a"] | ["b", "hello-b"] | "c">;
// type Test2 = {
//     c: unknown;
//     "hello-a": number;
//     "hello-b": string;
// }

pick<> as defined is backwards-compatible with the existing Pick<>, but does not allow for using as.

I think you'd probably need some compiler special treatment to allow for the as keyword instead of the tuple.

RyanCavanaugh commented 6 months ago

Introducing new syntax that only works in one generic type isn't really how languages work. This doesn't fit well with anything else and is already accomplishable through other means, as noted above.