0

I am making a type to enforce that this DynamicColor type is used correctly.

enum ColorsEnum {
  red = "red",
  green = "green",
  blue = "blue",
  yellow = "yellow",
}

type ColorsMapperType = {
  type: ColorsEnum
  red: {
    redProperty: string
  }
  green: {
    greenProperty: string
  }
  blue: {
    blueProperty: string
  }
  yellow: {
    yellowProperty: string
  }
}

type ColorsMapper = Omit<ColorsMapperType, "type">

export type DynamicColor = {
  [ColorType in keyof ColorsMapper]: {
    [Prop in 'type' | ColorType]: Prop extends 'type' ? ColorType : ColorsMapper[ColorType]
  }
}[keyof ColorsMapper]

Codesandbox: https://codesandbox.io/s/naughty-sanderson-niqez?file=/src/index.ts

I have it mostly working, however I want the type field in the DynamicColor type to be the correct ColorEnum type.

Right now, I get autocomplete for the string values of ColorEnum, but not the actual enum type.

For example, the following code should be valid:

const color: DynamicColor = {
  type: ColorsEnum.green,
  green: {
    greenProperty: "1",
  },
}

None of the following code should be valid

const color0: DynamicColor = {
  type: "green", // invalid because not of ColorsEnum type
  green: {
    greenProperty: "1",
  },
}

const color1: DynamicColor = {
  type: ColorsEnum.blue, // invalid because property "blue" is not included
}

const color2: DynamicColor = {
  type: ColorsEnum.blue, // invalid because color property does not match correct type (ColorsEnum.blue should require a "blue" property)
  green: {
    greenProperty: "1",
  }
}

const color3: DynamicColor = {
  type: ColorsEnum.blue, // invalid because more than one color property is included
  blue: {
    blueProperty: "1",
  }
  green: {
    greenProperty: "1",
  }
}

Is there a way to do this? Thanks in advance, and if there are other ways to improve this code I'm all ears.

2
  • Do I understand correctly that when the type in a ColorMapper object is blue, the only property it has is blue? I.e. you're discriminating the fields by the type? If so, you could just make separate types for the correct type-to-property combo and create an union type: typescriptlang.org/docs/handbook/… Commented Jan 14, 2021 at 17:36
  • @cbr that's correct, however the ColorsMapperType type (I renamed it from ColorMapper) has constraints in our domain and needs to stay as the same model. I'm hoping to do this without changing that type at all. Specifically, the type is being used as a GraphQL input type. GraphQL doesn't allow union input types, so we needed to resort to something like what I have above. Commented Jan 14, 2021 at 17:57

1 Answer 1

2

You basically want to map all items in the ColorsEnum to another type like so (pseudocode) { type: T; [T]: ColorsMapper[T] }. Fortunately, that is possible with conditional types. Here is a solution I found.

enum ColorsEnum {
  red = "red",
  green = "green",
  blue = "blue",
  yellow = "yellow",
}

type ColorsMapper = {
  type: ColorsEnum
  red: {
    redProperty: string
  }
  green: {
    greenProperty: string
  }
  blue: {
    blueProperty: string
  }
  yellow: {
    yellowProperty: string
  }
}
type MapToColorType<T> = T extends ColorsEnum ? { [key in T]: ColorsMapper[T] } & { type: T } : never;
type Result = MapToColorType<ColorsEnum>;

const color: Result = {
  type: ColorsEnum.green,
  green: {
    greenProperty: "1",
  },
}

You can play around with it here.

Explanation

The thing is that conditional types are distributive on union types. That means that the condition is applied to all members of the union. Enums are basically union of all the possible values. We can leverage that to map every member of the enum to a particular type.

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

Comments

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.