1
votes

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

1 Answers

2
votes

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.