3

I am using TypeScript to define a constructor for a model in Angular. One of the properties in the model is set as an enum with a few possible string values. This works fine if I pass an enum value to the constructor. The problem is that I need call the constructor based on an API response which returns a string for the value that needs to be mapped to that property.

Is there any way of passing a string (as long as it is one of the values defined in the enum) to the constructor?

Enum:

export enum TestTypes {
    FIRST = 'first',
    SECOND = 'second',
}

export class Test {
    this.myType: TestTypes;

    constructor(options: {type: TestTypes}) {
        this.myType = options.type;
    }
}

The following works const a = new Test({type:TestTypes.FIRST});

What I would like to achieve: const b = new Test({type:'first'})

Should I do the following? const b = new Test({type:TestTypes['first']})

2
  • 1
    const b = new Test({type:TestTypes['first']})? Commented Jul 20, 2018 at 0:30
  • const b=new Test({type:TestTypes.FIRST}); Commented Jul 20, 2018 at 6:33

1 Answer 1

3

The easiest way to go is to change your enum to a straight dictionary like this:

const literal = <L extends string | number | boolean>(l: L) => l;

export const TestTypes = {
  FIRST: literal('first'),
  SECOND: literal('second'),
};

export type TestTypes = (typeof TestTypes)[keyof typeof TestTypes]

The literal() function is a helper that prompts the compiler to interpret the value as a string literal instead of widening to string.

Now, the value TestTypes.FIRST is exactly the string "first", the value TestTypes.SECOND is exactly the string "second", and the type TestTypes is exactly the union "first"|"second". That lets your class work as desired:

export class Test {
  myType: TestTypes; // this is an annotation, not an initializer, right?

  constructor(options: { type: TestTypes }) {
    // options.type, not type  
    this.myType = options.type;
  }
}

const a = new Test({ type: TestTypes.FIRST }); // okay
const b = new Test({ type: "first" }); // okay... it's the same thing

If you want to keep TestTypes as an enum, you can get what you want, but it's too much hoop jumping in my opinion.

First, if you wanted a standalone function that accepted either the enum or the right string values, you could make a generic function like this:

declare function acceptTestTypesOrString<E extends string>(
  k: E & (Extract<TestTypes, E> extends never ? never : E)
): void;

I don't know if I should explain that, but it takes advantage of the fact that, say, TestTypes.FIRST extends "first". Let's see if it works:

acceptTestTypesOrString(TestTypes.FIRST) // okay
acceptTestTypesOrString(TestTypes.SECOND) // okay
acceptTestTypesOrString("first") // okay
acceptTestTypesOrString("second") // okay
acceptTestTypesOrString("third") // error

Looks good. But you want this as a constructor function. And you can't make a constructor function generic. Instead you could make the whole class generic, like this:

export class Test<E extends string> {
  myType: E; // this is an annotation, not an initializer, right?

  constructor(options: { 
    type: E & (Extract<TestTypes, E> extends never ? never : E) 
  }) {
    // options.type, not type  
    this.myType = options.type;
  }
}

const a = new Test({ type: TestTypes.FIRST }); // okay
const b = new Test({ type: "first" }); // also okay

In this case, a will be of type Test<TestTypes.FIRST> and b will be of type Test<"first">. They are mostly interchangeable, but it seems suboptimal to drag around a generic type for the whole class when you only want it for the constructor.

But it works.


Okay, hope one of those ideas help. Good luck!

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

2 Comments

Is it possible to go the second way (with enum) in TS 2.9, without the unknown type? Is it ever necessary?
Oh I forgot that unknown won't appear until TS3.0. Changed it.

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.