Nimelrian
Nimelrian

Reputation: 1716

How can I conditionally choose types depending on whether a property exists in another type?

First off, I want the following result:

type Wrapper<ID extends string> = { id: ID };
type WrapperWithPayload<ID extends string, Payload> = { id: ID, payload: Payload };

enum IDs {
  FOO = "ID Foo",
  BAR = "ID Bar",
  BAZ = "ID Baz"
}

interface AssociatedTypes {
  [IDs.FOO]: number;
  [IDs.BAR]: number[];
}

type Result = MagicMapper<IDs, AssociatedTypes>
/*
 * Result should have this type:
 * {
 *   [IDs.FOO]: WrapperWithPayload<IDs.FOO, number>;
 *   [IDs.BAR]: WrapperWithPayload<IDs.BAR, number[]>;
 *   [IDs.BAZ]: Wrapper<IDs.BAZ>;
 * }
 */

In other words, I want to supply some strings and a mapping, which maps a subset of those strings to another type. Then, I want to create a new type using the strings as keys. For each of the strings/keys, if a mapping exists, I want to use type A, and otherwise I want to use type B.

Right now, my approach looks like this:

type MagicMapper<T extends string, Mapping extends { [key in T]?: any }> = {
    [key in T]: Mapping[key] extends never ? Wrapper<key> : WrapperWithPayload<key, Mapping[key]>;
};

And that one almost hits the goal:

type Result = {
  "ID Foo": WrapperWithPayload<IDs.FOO, number>;
  "ID Bar": WrapperWithPayload<IDs.BAR, number[]>;
  "ID Baz": Wrapper<IDs.BAZ> | WrapperWithPayload<IDs.BAZ, any>;
}

The union on the Baz key is wrong. I think the error lies within the extends never condition, but replacing it with, e.g., undefined makes things only worse:

type MagicMapper<T extends string, Mapping extends { [key in T]?: any }> = {
    [key in T]: Mapping[key] extends undefined ? Wrapper<key> : WrapperWithPayload<key, Mapping[key]>;
};

type Result = {
  "ID Foo": Wrapper<IDs.FOO>;
  "ID Bar": Wrapper<IDs.BAR>;
  "ID Baz": Wrapper<IDs.BAZ>;
}

Is there any way to make things work the way I need?

Upvotes: 3

Views: 1646

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249536

You can change the test for the existence of the key in Mapping to Mapping extends { [P in key]: infer U } and everything will work as expected:

type Wrapper<ID extends string> = { id: ID };
type WrapperWithPayload<ID extends string, Payload> = { id: ID, payload: Payload };

enum IDs {
    FOO = "ID Foo",
    BAR = "ID Bar",
    BAZ = "ID Baz"
}

interface AssociatedTypes {
    [IDs.FOO]: number;
    [IDs.BAR]: number[];
}

type MagicMapper<T extends string, Mapping extends { [key in T]?: any }> = {
    [key in T]: Mapping extends { [P in key]: infer U } ? WrapperWithPayload<key, U>: Wrapper<key> ;
};
type Result = MagicMapper<IDs, AssociatedTypes> 
// same as :
type Result = {
    "ID Foo": WrapperWithPayload<IDs.FOO, number>;
    "ID Bar": WrapperWithPayload<IDs.BAR, number[]>;
    "ID Baz": Wrapper<IDs.BAZ>;
}

Playground link

Upvotes: 1

Related Questions