2

Today I created two new UnionTypes in my project.

You can see this types here in Code-Review: https://codereview.stackexchange.com/questions/223433/uniontypes-partialrequired-and-pickrequired

export type PartialRequired<T, K extends keyof T> = Partial<T> & Pick<Required<T>, K>;
export type PickRequired<T, K extends keyof T> = T & Pick<Required<T>, K>;
export type ForceId<T extends { id?: ID }, ID = number> = PickRequired<T, 'id'>;

Now I thought about to make the id attribute renameable?!

I tried something like this:

export type ForceId<T extends { [key: ID_NAME]?: ID }, ID = number, ID_NAME extends string = 'id'> = PickRequired<T, ID_NAME>;
//                                            ~~ ';' expected

But as you can see, this didn't work. Is there a way I can achive something like this? So I can use ForceId<MyModel, string, 'uuid'> => { uuid: '123e4567-e89b-12d3-a456-426655440000' } without create a new definition like ForceUuid?

TypeScript Playground

Edit:

The goal is the following:

I have some Models looking like this:

interface MyModel1 {
  id?: number; // The identifier of Model 1
  name: string;
  age?: number;
  alive?: boolean;
}

interface MyModel2 {
  uuid?: string; // The identifier of Model 2
  name: string;
  age?: number;
  alive?: boolean;
}

I don't want to change any code at runtime.

Now I want to use the ForceId type.

// Works
const model1: ForceId<MyModel1> = {
  id: 0,
  name: "test",
  age: 10
};

// Don't work
const model2: ForceId<MyModel2, string, "uuid"> = {
  uuid: "123e4567-e89b-12d3-a456-426655440000",
  name: "test",
  age: 10
};
Shinigami
  • 646
  • 1
  • 7
  • 21

1 Answers1

3

I'm going to answer the general question about how to programmatically modify an object type to rename keys.

Here's one possible implementation that only really works for required, mutable properties with known literal keys (no optional properties, no readonly properties, and no index signatures). If you need to support those cases it is possible but gets even more ugly, so I'm ignoring it for now.

type ValueOf<T> = T[keyof T];

type RenameKeys<T, M extends Record<keyof any, keyof any>> = ValueOf<
  { [K in keyof T]: (x: Record<K extends keyof M ? M[K] : K, T[K]>) => void }
> extends ((x: infer R) => void)
  ? { [K in keyof R]: R[K] }
  : never;

That might be confusing, but basically it takes an object type T and a key-mapping type M, and produces a new type with the keys renamed but the property types the same. For example:

interface MyModel {
  id: number;
  name: string;
  age: number;
  alive: boolean;
}

type Renamed = RenameKeys<MyModel, { id: "uuid" }>;

produces

type Renamed = {
    uuid: number;
    name: string;
    age: number;
    alive: boolean;
}

and

type RenamedMulti = RenameKeys<
  MyModel,
  { id: "uuid"; alive: "notDead"; age: "yearsOld" }
>;

produces

type RenamedMulti = {
    uuid: number;
    name: string;
    yearsOld: number;
    notDead: boolean;
}

You might be able to use RenameKeys to build up the types you're looking for.


As for how it works:

type RenameKeys<T, M extends Record<keyof any, keyof any>> = ValueOf<
  { [K in keyof T]: (x: Record<K extends keyof M ? M[K] : K, T[K]>) => void }
> extends ((x: infer R) => void)
  ? { [K in keyof R]: R[K] }
  : never;

The M mapping extending Record<keyof any, keyof any> just makes sure that it's a map from keys to keys. Then, let's imagine this: {[K in keyof T]: Record<K extends keyof M ? M[K] : K, T[K]>]}. That basically takes every property in T and looks up in M to see if the key is mapped, and if so, maps it... If you did that where T was MyModel and M was {id: "uuid"}, then you'd get {id: {uuid: number}, name: {name: string}, age: {age: number}, alive: {alive: boolean}}.

It's a bit tricky to get from that to a type with those combined... the way I do it is with conditional type inference to turn a union of those types into an intersection. First I put those properties into function parameters, like (x: Record<...> => void). Then I get a union of the functions (that's the ValueOf<> application) and infer a single function with parameter type R from it. That ends up being an intersection, since (x: A)=>void | (x: B)=>void is assignable to (x: A & B) => void. (Another explanation of how this works is here or possibly here)

So the type R that comes out looks like {uuid: number} & {name: string} & {age: number} & {alive: boolean}, and then {[K in keyof R]: R[K]} is an "identity" mapped type that collects those properties together into {uuid: number; name: string; age: number; alive: boolean}.


Okay, hope that helps; good luck!

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Ohhh you were so close! I extended your example and wrapped the Record with a Partial https:// bit.ly/2J7iD2W The url was to long and Stack Overflow disallowes url shortener. So please remove the whitespace before bit – Shinigami Jul 03 '19 at 19:42
  • If you want, you can update your answer and then I will accept it :) – Shinigami Jul 03 '19 at 19:49
  • I guess I'm confused, since the code you have in that url isn't renaming any keys; I might have completely misunderstood your question. If the question is just "how do I make an object with an arbitrary literal key" then the answer is probably just "use [`Record`](https://stackoverflow.com/questions/51936369/what-is-the-record-type-in-typescript/51937036#51937036)" or the equivalent `{[P in K]: V}` definition. – jcalz Jul 03 '19 at 23:45
  • I think I found out why we misunderstood each other. I told you that MyModel is something like {id:...}, but it can be also something like {uuid:...}. I updated my question to make some clearance of my goal. – Shinigami Jul 04 '19 at 07:19