0

in JavaScript I often (ab)use objects as pseudo-enums:

const application = {
    ELECTRIC: {propA: true, propB: 11, propC: "eee"},
    HYDRAULIC: {propA: false, propB: 59, propC: "hhh"},
    PNEUMATIC: {propA: true, propB: 87, propC: "ppp"},
}

const foo = application.ELECTRIC
const bar = application.HYDRAULIC

I'd like to achieve a similar behavior in TypeScript, but in a type-safe manner. Here's what I have so far:

type Application = "ELECTRIC" | "HYDRAULIC" | "PNEUMATIC";

interface ApplicationProps {
  propA: boolean;
  propB: number;
  propC: string;
}

const application: Record<Application, ApplicationProps> = {
    ELECTRIC: {propA: true, propB: 11, propC: "eee"},
    HYDRAULIC: {propA: false, propB: 59, propC: "hhh"},
    PNEUMATIC: {propA: true, propB: 87, propC: "ppp"},
}

const foo = application.ELECTRIC;
---
let bar: ApplicationProps; // an attempt to restrict bar to application.ELECTRIC|application.HYDRAULIC|application.PNEUMATIC
bar = application.HYDRAULIC;

That works okay but feels clumsy.

1) There seems to be lot of repetition in type/interface/object

Is there some TS-Trickery to automatically infer type Application / interface ApplicationProps from the object? Like some keyof typeof something magic?

2) It feels strange to type bar as ApplicationProps when I actually want to restrict it to members of application

Is there a more elegant way to restrict bar to application.ELECTRIC|application.HYDRAULIC|...?

3) If I want to define bar in another file, I need to import ApplicationProps and application. I know the type info is part of application but I don't know how to get it.

The best I got so far was: let bar: typeof application[keyof typeof application] Which of course is stupid. There has to be a better way?!



Thanks in advance :)
2
  • 1
    The "standard" way to do this is with a const assertion for the enumlike object, and a type alias of the form type Enum = typeof Enum[keyof typeof Enum] for the type. Like this in your case. Does that meet your needs? If so I could write up an answer; if not, what am I missing? I might have just written this up directly, but your line "which of course is stupid" gives me pause, given that the "stupid" thing is part of my suggestion. Can you articulate why it's stupid? Commented Jul 8, 2022 at 13:37
  • I like your simple solution! I don't know if it is a good way to solve the problem but please write it as an answer. Maybe others can/will comments on it :) ... PS: I don't know if it is really 'stupid' but my approach felt very desperate... Commented Jul 11, 2022 at 8:44

2 Answers 2

1

So you can do something like this:


// all application keys will be defined here
// as const is needed here so typescript won't start widening the type to string[]
const applications = ["ELECTRIC", "HYDRAULIC", "PNEUMATIC"] as const

// get all keys as a Union Type
type ApplicationKeys = typeof applications[number]
// create your enum object as a type
type Application = { [Key in ApplicationKeys]: ApplicationProps<Key> }
// define your props with a key so typescript can differentiate between your enums
type ApplicationProps<T> = {
  key: T,
  propA: boolean;
  propB: number;
  propC: string;
}
// the actual enum definition
const application: Application = {
  ELECTRIC: { key: "ELECTRIC", propA: true, propB: 11, propC: "eee" },
  HYDRAULIC: { key: "HYDRAULIC", propA: false, propB: 59, propC: "hhh" },
  PNEUMATIC: { key: "PNEUMATIC", propA: true, propB: 87, propC: "ppp" },
}
// test
let foo = application.ELECTRIC;
let bar: Application["PNEUMATIC"]; 
bar = application.HYDRAULIC; // error: Type 'ApplicationProps<"HYDRAULIC">' is not assignable to type 'ApplicationProps<"ELECTRIC">'. 
bar ===foo // error his condition will always return 'false' since the types have no overlap

The neat part is that if you want to add a new application key you just have to add it to the list and typescript will throw errors for the missing or unhandled key in a switch or if statement(exhaustive switch block)[https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript].

I hope you understand my bad english :) CODE

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

1 Comment

Your English is perfectly fine! I'm especially thankful for your code comments. Those helped to clarify some details/concepts. I don't see a way in your solution to type bar like this: let bar: Application // restrict bar to Application.ELECTRIC|Application.HYDRAULIC|Application.PNEUMATIC --- // later in the code assign a concrete Application member: bar = Application.ELECTRIC --- Your post is good - I learned something. But in the end, you use even more types to solve the problem and I was looking for simplification. Which might not be possible atm.. Thanks anyway :)
1

I sometimes do something like this, with having a enum list mapping to specific types.

But I'm also curious about how this could be done better.

enum ApplicationType {
  ELECTRIC = 'ELECTRIC',
  HYDRAULIC = 'HYDRAULIC',
  PNEUMATIC = 'PNEUMATIC',
}

interface Application {
  propA: boolean;
  propB: number;
  propC: string;
}

interface ElectricApplication extends Application {
  propD: string;
}
interface PneumaticApplication extends Application {}
interface HydaulicApplication extends Application {}

type ApplicationTypeMap = {
  [ApplicationType.ELECTRIC]: ElectricApplication;
  [ApplicationType.HYDRAULIC]: HydaulicApplication;
  [ApplicationType.PNEUMATIC]: PneumaticApplication;
};

const applications: { [key in ApplicationType]: ApplicationTypeMap[key] } = {
  ELECTRIC: {
    propA: true,
    propB: 11,
    propC: 'eee',
    propD: 'boo',
  },
  HYDRAULIC: { propA: false, propB: 59, propC: 'hhh' },
  PNEUMATIC: { propA: true, propB: 87, propC: 'ppp' },
};

const foo = applications[ApplicationType.ELECTRIC];
console.log(foo.propD);

const bar = applications[ApplicationType.HYDRAULIC];
console.log(bar.propA);

1 Comment

Thanks for your contribution! Your solution has some nifty details but overall (a) the complexity is even bigger and (b) I can't figure out how to type bar like this: let bar: Application // restrict bar to Application.ELECTRIC|Application.HYDRAULIC|Application.PNEUMATIC --- // later in the code assign a concrete Application member: bar = Application.ELECTRIC

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.