Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changing Property Name in Typescript Mapped Type

Tags:

I have a collection of Typescript objects that look like this:

SomeData {   prop1: string;   prop2: number; } 

And I need to end up with some objects that look like this:

type SomeMoreData= {   prop1Change: string;   prop2Change: number; } 

I know that if I just wanted to change the type, I could do this:

type SomeMoreData<T> = {   [P in keyof T]: boolean; } 

Which would give me:

SomeMoreData<SomeData> {   prop1: boolean;   prop2: boolean; } 

Is there a way to manipulate the name produced by the [P in keyof T] bit? I've tried things like [(P in keyof T) + "Change"] with no success.

like image 413
Tony Avatar asked Jun 02 '17 07:06

Tony


People also ask

What is mapped type in TypeScript?

A mapped type is a generic type which uses a union of PropertyKey s (frequently created via a keyof ) to iterate through keys to create a type: type OptionsFlags < Type > = { [ Property in keyof Type ]: boolean; };

How do you omit a property in TypeScript?

Use the Omit utility type to exclude a property from a type, e.g. type WithoutCountry = Omit<Person, 'country'> . The Omit utility type constructs a new type by removing the specified keys from the existing type. Copied!


2 Answers

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

like image 111
jcalz Avatar answered Sep 28 '22 10:09

jcalz


Key remapping in Mapped Types was introduced in typescript 4.1:

type SomeData = {     prop1: string;     prop2: number; }  type WithChange<T> = { [P in keyof T & string as `${P}Change`]: T[P] };  type SomeMoreData = WithChange<SomeData>; // {  prop1Change: string, prop2Change: number } 

Playground


In above example we use two new features: as clause in mapped types + template literal type.

Template string types are the type space equivalent of template string expressions. Similar to template string expressions, template string types are enclosed in backtick delimiters and can contain placeholders of the form ${T}, where T is a type that is assignable to string, number, boolean, or bigint. Template string types provide the ability to concatenate literal strings, convert literals of non-string primitive types to their string representation, and change the capitalization or casing of string literals. Furthermore, through type inference, template string types provide a simple form of string pattern matching and decomposition.

Some examples:

type EventName<T extends string> = `${T}Changed`; type Concat<S1 extends string, S2 extends string> = `${S1}${S2}`; type T0 = EventName<'foo'>;  // 'fooChanged' type T1 = EventName<'foo' | 'bar' | 'baz'>;  // 'fooChanged' | 'barChanged' | 'bazChanged' type T2 = Concat<'Hello', 'World'>;  // 'HelloWorld' type T3 = `${'top' | 'bottom'}-${'left' | 'right'}`;  // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' 

Additionally mapped types support an optional as clause through which a transformation of the generated property names can be specified:

{ [P in K as N]: X } 

where N must be a type that is assignable to string.

like image 37
Aleksey L. Avatar answered Sep 28 '22 09:09

Aleksey L.