17

I have this:

type Two = {
  one: number,
  two: string,
  three: boolean
}

I want it to create a type that would look like this:

type RenamedTwo = {
  one: number,
  two: string,
  four: boolean // difference
}

Tried to create it this way:

type Rename<T, K extends keyof T, N> = Pick<T, Exclude<keyof T, K>> & { [N]: T[K] }

In an attempt to use this way:

type Renamed = Rename<Two, 'three', 'four'>

But TSlint marks [N] as error and gives this error message:

[ts] A computed property name in a type literal must refer to an expression whose type is a literal type or a 'unique symbol' type. [ts] 'N' only refers to a type, but is being used as a value here.

Eduard
  • 8,437
  • 10
  • 42
  • 64
  • In case someone wants to rename multiple properties, here is the type definition: `type Rename = Pick> & Record>>` – Eduard Oct 09 '18 at 08:06

3 Answers3

20

You need to use a mapped type for the renamed property as well:

type Two = {
    one: number,
    two: string,
    three: boolean
}


type Rename<T, K extends keyof T, N extends string> = Pick<T, Exclude<keyof T, K>> & { [P in N]: T[K] }

type Renamed = Rename<Two, 'three', 'four'>

Note that this will not work as expected if you provide more properties:

type Renamed = Rename<Two, 'two'  |'three' , 'four' | 'five'> // will be Pick<Two, "one"> & {
//    four: string | boolean;
//    five: string | boolean;
// }
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • 1
    Thank you! Especially for the note. I adjusted the type definition based on your feedback: `type Rename = Pick> & Record>>` – Eduard Oct 09 '18 at 08:05
  • Awesome. TIL you can use `in` operator in a `string` type or Typescript reads it as a union type even if it is of type string? – The.Wolfgang.Grimmer Jun 22 '22 at 08:12
9

In the current typescript version 4.6.2, there is a remapping gramma to use. It can be implement this much easier.

PlayGround

type RenameByT<T, U> = {
  [K in keyof U as K extends keyof T
    ? T[K] extends string
      ? T[K]
      : never
    : K]: K extends keyof U ? U[K] : never;
};

type Two = { one: number; two: string; three: boolean };

// retrunType = { one: number; two: string, four: boolean };
type renameOne = RemaneByT<{three: 'four', five: 'nouse'}, Two>;

// returnType = { x: number, y: string, z: boolean; }
type renameAll = RenameByT<{one: 'x', two: 'y', three: 'z'}, Two>;

The RenameByT type can separate by several parts. The explain is for the example renameOne

  1. K in keyof U means all the key in U. For the example is one | two | three
  2. as clause since ts4.1 can use. but i can't use in 4.4.4 but can use in 4.6.2. This is use to rename Key K by the condiction type
  3. K extends keyof T. keyof T means three | five.
  4. T[K] extends string means T[K] is string. T['three'] is string so returns four, T['five'] returns nouse;
  5. the return type K extends keyof U, so T['three'] is satisfy so return { four: U['three'] } means { four: boolean}
  6. ohters keys return as origin

reference:

https://github.com/microsoft/TypeScript/issues/40833 https://www.typescriptlang.org/docs/handbook/2/mapped-types.html

banana
  • 91
  • 1
  • 3
1

It is perhaps worth mentioning that you can solve the OP's original problem without writing a generic type like Rename. If you just want to do it once, you could do something much simpler.

type RenamedTwo = { [Property in keyof Two as Property extends 'three' ? 'four': Property]:  Two[Property] }
Tom Greenwood
  • 1,502
  • 14
  • 17