Changing Property Name in Typescript Mapped Type

Pre-TS4.1 answer;

You can’t do it automatically. The big blocker is that there is currently no type operator that lets you append string literals at the type level, so you can’t even describe the transformation you’re doing:

// without Append<A extends string, B extends string>, you can't type this:

function appendChange<T extends string>(originalKey: T): Append<T,'Change'> {
  return originalKey+'Change';
}

There is a suggestion for this feature, but who knows if it will happen.

That means if you want to transform keys, you need to actually hard-code the specific mapping you’re looking for, from string literal to string literal:

type SomeMoreDataMapping = {
  prop1: "prop1Change"
  prop2: "prop2Change"
}

Armed with this mapping, you can define these:

type ValueOf<T> = T[keyof T]
type KeyValueTupleToObject<T extends [keyof any, any]> = {
  [K in T[0]]: Extract<T, [K, any]>[1]
}
type MapKeys<T, M extends Record<string, string>> =
  KeyValueTupleToObject<ValueOf<{
    [K in keyof T]: [K extends keyof M ? M[K] : K, T[K]]
  }>>

Brief runthrough:

  • ValueOf<T> just returns the union of property value types for type T.
  • KeyValueTupleToObject takes a union of tuple types like this: ["a",string] | ["b",number] and turns them into an object type like this: {a: string, b: number}.
  • And MapKeys<T, M> takes a type T and a key-mapping M and substitutes any key in T which is present in M with the corresponding key from M. If a key in T is not present in M, the key is not transformed. If a key in M is not present in T, it will be ignored.

Now you can (finally) do this:

type SomeMoreData= MapKeys<SomeData, SomeMoreDataMapping>;

And if you inspect SomeMoreData, you see it has the right type:

var someMoreData: SomeMoreData = {
  prop1Change: 'Mystery Science Theater',
  prop2Change: 3000
} // type checks

This should allow you to do some fun things like:

function makeTheChange<T>(input: T): MapKeys<T, SomeMoreDataMapping> {
  var ret = {} as MapKeys<T, SomeMoreDataMapping>;
  for (var k in input) {
    // lots of any needed here; hard to convince the type system you're doing the right thing
    var nk: keyof typeof ret = <any>((k === 'prop1') ? 'prop1Change' : (k === 'prop2') ? 'prop2Change' : k);
    ret[nk] = <any>input[k];    
  }
  return ret;
}

var changed = makeTheChange({ prop1: 'Gypsy', prop2: 'Tom', prop3: 'Crow' });
console.log(changed.prop1Change.charAt(0)); //ok
console.log(changed.prop2Change.charAt(0)); //ok
console.log(changed.prop3.charAt(0)); //ok

Hope that helps. Good luck!

UPDATED SEP 2018 to use advantage of conditional types introduced with TS 2.8


Another update: if you want this to work with optional properties it gets more complicated:

type RequiredKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K }[keyof T];
type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T];
type MapKeys<T, M extends Record<string, string>> =
  KeyValueTupleToObject<ValueOf<{
    [K in RequiredKeys<T>]-?: [K extends keyof M ? M[K] : K, T[K]]
  }>> & Partial<KeyValueTupleToObject<ValueOf<{
    [K in OptionalKeys<T>]-?: [K extends keyof M ? M[K] : K, T[K]]
  }>>> extends infer O ? { [K in keyof O]: O[K] } : never;

If you need to support index signatures it would get even more complicated.

Playground link to code

Leave a Comment