1

Currently I have the following types:

type PossibleKeys = number | string | symbol;
type ValueOf<T extends object> = T[keyof T]; 
type ReplaceKeys<T extends Record<PossibleKeys, any>, U extends Partial<Record<keyof T, PossibleKeys>>> = 
  Omit<T, keyof U> & { [P in ValueOf<U>]: T[keyof U] };

...but, although it works even partially, it gives the following error:

Type 'U[keyof U]' is not assignable to type 'string | number | symbol'.


A simple demo:

interface Item {
  readonly description: string;
  readonly id: string;
}

interface MyInterface {
  readonly id: string;
  readonly propToReplace: number;
  readonly anotherPropToReplace: readonly Item[];
}

type ReplacedUser = ReplaceKeys<MyInterface, { propToReplace: 'total', anotherPropToReplace: 'items' }>;

In ReplacedUser I can see that the type is almost correct. The inferred type is:

{ id: string; total: number | readonly Item[]; items: number | readonly Item[]; }

... while I'm expecting:

{ id: string; total: number; items: readonly Item[]; }

What am I doing wrong? I'd like to know first how I can express that P needs to get the values passed in U to suppress Typescript errors and after that get the correct type for a specific value.

1 Answer 1

2

The simplest approach is to invert the U type parameter for your ReplaceKeys:

type PossibleKeys = number | string | symbol;
type ReplaceKeys<T extends {}, U extends Record<PossibleKeys, keyof T>> = Omit<T, ValueOf<U>> & {
    [K in keyof U]: T[U[K]]
};

Which you can then use like this:

type ReplacedUser = ReplaceKeys<MyInterface, { total: 'propToReplace', items: 'anotherPropToReplace' }>;

If though you cannot change the shape of the U thing become a bit trickier:

// Example types from your post
interface Item {
  readonly description: string;
  readonly id: string;
}

interface MyInterface {
  readonly id: string;
  readonly propToReplace: number;
  readonly anotherPropToReplace: readonly Item[];
}

// All possible key types
type PossibleKeys = number | string | symbol;

// Helper type to get all non-nullable values from type T
type DefinedValues<T> = NonNullable<T[keyof T]>;

// Helper type for your replacements object - a record whose values are valid keys
// and whose keys are also present in type T (the input type)
// 
// Partial is used to make sure you don't need to pass all keys of T in your replacements
type Replacements<T extends {}> = Partial<Record<keyof T, PossibleKeys>>;

// Helper type that swaps object keys for values
type Invert<T extends Replacements<C>, C extends {} = {}> = {
  [N in DefinedValues<T>]: {
    [K in keyof T]: N extends T[K] ? K : never
  }[keyof T]
}

type ReplacedKeys<T extends {}, R extends Replacements<T>> = Omit<T, keyof R | DefinedValues<R>> & {
  [N in keyof Invert<R>]: {
    [L in keyof R]: N extends R[L] ? (L extends keyof T ? T[L] : never) : never;
  }[keyof R]
}

Be aware thought that using the second approach does not warn you about having duplicate mappings:

type ReplacedUser = ReplacedKeys<MyInterface, { propToReplace: 'total', anotherPropToReplace: 'total' }>;

Check the playground here.

Sign up to request clarification or add additional context in comments.

6 Comments

Excellent, thanks for the such detailed explanation! The second approach is what I was looking for :).
Just for record, to remove the error in my type definition I should have used NonNullable<ValueOf<U>> (maybe more clear error messages could help :/), but of course, it doesn't solve the second problem, which is nicely solved above :)
Hmm, I just noticed that with the second approach I can pass any value as key, not only the keyof T. Do you know how to fix this? Ex: ReplaceKeys<MyInterface, { strangeKey: 'newKey', anotherPropToReplace: 'total' }> should error in strangeKey, but it currently doesn't.
I updated the answer, switched the "direction" of type constrain around :) check it out
Nice, that works! I tested and it works, but for some reason, when I try to use it as a ReturnType of a function, it doesn't. Can you help me with this last part? I've placed this in this playground -> bit.ly/2Ae5lj4 (I had to shorten it to fit here because playground generates a really long url).
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.