yaserso
yaserso

Reputation: 2868

How to Pick and rename certain keys using Typescript?

I have an interface User:

interface User {
    _id     : string;
    name    : string;
    email   : string;
    password: string;
    phone   : number;
}

I have another interface UpdatedBy:

interface UpdatedUser {
    id  : string;
    name: string;
}

I know I can use Pick, but I want to rename _id to id in the UpdatedUser interface.

type UpdatedUser = Pick<User, '_id' | 'name'>; // How can I turn _id into id?

Update: I basically want to do a cleaner version of this:

export interface UpdatedUser extends Pick<User, 'name'> {
    id  : Extract<User, '_id'>;
}

Upvotes: 29

Views: 19946

Answers (5)

Qwerty
Qwerty

Reputation: 31949

I have another, much simpler, solution which is easier to use with a syntax inspired by destructuring. It also works well with intellisense when picking the keys from the original object.

Screenshot from VSCode showing property key suggestions when using Pick.

typescript playground

Inspiration
const {a: x, b: y} = {a: 1, b: 'b'}
// x = 1
// y = 'b'

Proposal

type A = { a: string, b: number, c: boolean, d: any }
type B = PickAs<A, 'a:x' | 'b:y' | 'c'>
// B = { x: string; y: number; c: boolean; }

Solution

type PickAs<T, K extends Exclude<keyof T, K extends `${infer A}:${string}` ? A : never>> =
  Normalize<
    & UnionToIntersection<K extends `${infer A}:${infer B}` ? { [key in B]: T[A] } : never>
    & Pick<T, Extract<K, keyof T>>
  >

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends 
  (k: infer I) => void ? I : never

type Normalize<T> = { [K in keyof T]: T[K] }

How does this work?

The stripped type looks like this:

type PickAs<T, K extends keyof T> = // K = e.g. `'a:x' | 'b:y' | 'c'`
  K extends `${infer A}:${infer B}` // 1
    ? { [P in B]: T[A] } // 2
    : never  // 3 - ignore everything else

// Compare to classic Pick
type Pick<T, K extends keyof T> =
      { [P in K]: T[P] }
  1. If K extends a string *:*, extract it's constituents into A and B
  2. Then construct a new type { [B]: T[A] }
  3. However, this only works for pairs 'a:c' | 'b:d', but not for 'a' | 'b'.

To support original keys of T , we add

Pick<T, Extract<K, keyof T>> // From `K` only extract original keys of `T` and Pick.

The resulting type may look like this

type A = { a: string, b: number, c: boolean, d: any }
type B = PickAs<A, 'a:x' | 'b:y' | 'c'>

// result
type B = ({
    x: string;
} | {
    y: number;
}) & Pick<A, "c">

Not perfect.

  1. UnionToIntersection resolves {x} | {y} into {x} & {y}.
  2. Normalize merges {x} & {y} & Pick<A,'c'> into one object. { x, y, c }

And the last thing to do is to add intellisense support.

// Before:
type PickAs<T, K extends keyof T>;
// After:
type PickAs<T, K extends Exclude<keyof T, K extends `${infer A}:${string}` ? A : never> | `${string}:${string}`>;
  • If a key K matches ?:?, take the first constituent {A}:? and return it (? A : never) instead of the whole string ?:?. Exclude<keyof T, A> then removes it from the allowed pool of values, which makes it disappear from intellisense.
  • | `${string}:${string}` then adds the string back, so that the extends clause doesn't give error that abc:xyz "is not assignable to type keyof T"

Upvotes: 0

Lars Munkholm
Lars Munkholm

Reputation: 308

I really liked ford04's Dynamic version for multiple properties, but I wanted optional keys to remain optional, like in Alireza Mirian's answer.

So I've combined the two here.

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I,
) => void
    ? I
    : never;

export type RenameProperties<
    T,
    R extends {
        [K in keyof R]: K extends keyof T ? PropertyKey : "Error: key not in T";
    },
> = Omit<T, keyof R> &
    UnionToIntersection<
        {
            [P in keyof R & keyof T]: undefined extends T[P]
                ? { [PP in R[P]]?: T[P] }
                : { [PP in R[P]]: T[P] };
        }[keyof R & keyof T]
    >;

Upvotes: 2

Alireza Mirian
Alireza Mirian

Reputation: 6672

The problem with ford04's answer is that it doesn't keep optional/required property of renamed keys. Here is how you can rename a prop while accounting for the key being optional or required:

type KeyRenamed<T, K extends keyof T, R extends PropertyKey> = Omit<
  T,
  K
> &
  (undefined extends T[K] ? { [P in R]?: T[K] } : { [P in R]: T[K] });
type NameOptional = {
    name?: string;
}

type NameRequired = {
    name: string;
}

type DisplayNameRequired = KeyRenamed<NameRequired, 'name', 'displayName'>
type DisplayNameOptional = KeyRenamed<NameOptional, 'name', 'displayName'>

const displayNameOptional: DisplayNameOptional = { displayName: 'Ali' } 
const displayNameOptional_missing: DisplayNameOptional = {  } // no error, since displayName is kept optional after rename
const displayNameRequired: DisplayNameRequired = { displayName: 'Ali' } 
const displayNameRequired_missing: DisplayNameRequired = {  } // error, since displayName is kept required after rename

You can apply the same thing to those more advanced types in that answer, if for example you want to rename multiple keys at once.

playground

Upvotes: 1

ford04
ford04

Reputation: 74510

There is no built-in type for a renaming Pick, fortunately we can create one with reasonable effort.

Simple variant

type IdRenamed = Omit<User, "_id"> & { id: User["_id"] }
// { name: string; email: string; password: string; phone: number; id: string;}

Playground

Dynamic version for single property

type PickRename<T, K extends keyof T, R extends PropertyKey> =
    Omit<T, K> & { [P in R]: T[K] }

type T21 = PickRename<User, "_id", "id"> // same type as above
type T22 = PickRename<User, "foo", "id"> // error, foo is no property

Playground

TS 4.1 Alternative: use mapped type as clauses. Its advantage is that readonly or optional (?) modifiers of properties are preserved (see homomorphic mapped types 1, 2 for more details).

type PickRename<T, K extends keyof T, R extends PropertyKey> = {
    [P in keyof T as P extends K ? R : P]: T[P]
} // type instantiation same as previous example

Playground

Dynamic version for multiple properties

type PickRenameMulti<T, R extends
    { [K in keyof R]: K extends keyof T ? PropertyKey : "Error: key not in T" }
    > = Omit<T, keyof R> & UnionToIntersection<
        { [P in keyof R & keyof T]: { [PP in R[P]]: T[P] } }[keyof R & keyof T]
    >

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends
    ((k: infer I) => void) ? I : never

type T31 = PickRenameMulti<User, { _id: "id"; name: "firstName" }>
type T32 = PickRenameMulti<User, { foo: "id" }> // error, foo is no property

Note: See the great UnionToIntersection type for more details on the helper.

Playground

TS 4.1 again eases up syntax and produces homomorphic mapped types:
type PickRenameMulti<T, R extends
    { [K in keyof R]: K extends keyof T ? PropertyKey : "Error: key not in T" }
    > = { [P in keyof T as P extends keyof R ? R[P] : P]: T[P] }

Playground

TS 4.1: Drop _ prefix from all property keys

type DropUnderscore<T> = {
    [K in keyof T as K extends `_${infer I }` ? I : K]: T[K]
};
type T4 = DropUnderscore<User> // "_id" and "_email" renamed to "id", "email"

Playground

Upvotes: 56

Martin
Martin

Reputation: 1419

A slightly cleaner version would be ...

export interface UpdatedUser extends Pick<User, 'name'> {
  id: User['_id'];
}

... but I'm not sure how to rename it on-the-fly as you're suggesting. It's an interesting use-case.

Upvotes: 8

Related Questions