Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use a string enum type as a computed property name in an interface?

Tags:

typescript

I am modeling an API request format in TypeScript:

interface ApiTerm {
    term: {
        name: string,
        value: string,
    }
}

interface ApiAnd {
    and: {
        lhs: ApiItem,
        rhs: ApiItem,
    }
}

interface ApiOr {
    or: {
        lhs: ApiItem,
        rhs: ApiItem,
    }
}

type ApiItem =
    | ApiTerm
    | ApiAnd
    | ApiOr
    ;

This works, but I will need to implement many binary operations beyond just "and" and "or" so I'd like some way to shorten and reuse the code.

Following some other code I've written that uses a specific string enum value in an interface, I tried to use a string enum as the property name:

enum Operation {
    And = "and",
    Or = "or",
}

interface ApiBinary<O extends Operation> {
    [O]: {
        lhs: ApiItem,
        rhs: ApiItem,
    }
}

type ApiItem =
    | ApiTerm
    | ApiBinary<Operation.And>
    | ApiBinary<Operation.Or>
    ;

Unfortunately, this generates an error with TypeScript 2.9.1:

A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.

Is there an alternative solution that will allow me to avoid having to write out the numerous duplicate interfaces that will only differ by the name of the key?

Looking into the "a 'unique symbol' type" part of the error message, I don't believe I can create an enum based on Symbols.

Related questions

Use string enum value in TypeScript interface as a computed property key seems very close, but is about using specific string enum values in an interface, which works as-in in newer TypeScript versions.

like image 944
Shepmaster Avatar asked Jun 14 '18 03:06

Shepmaster


People also ask

How do I use string enums?

In summary, to make use of string-based enum types, we can reference them by using the name of the enum and their corresponding value, just as you would access the properties of an object. At runtime, string-based enums behave just like objects and can easily be passed to functions like regular objects.

Can I use enum as a type in TypeScript?

Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript. Enums allow a developer to define a set of named constants. Using enums can make it easier to document intent, or create a set of distinct cases. TypeScript provides both numeric and string-based enums.

Can enum be string?

Using string-based enums in C# is not supported and throws a compiler error. Since C# doesn't support enum with string value, in this blog post, we'll look at alternatives and examples that you can use in code to make your life easier. The most popular string enum alternatives are: Use a public static readonly string.

How do I convert a string to enum in TypeScript?

To convert a string to an enum: Use keyof typeof to cast the string to the type of the enum. Use bracket notation to access the corresponding value of the string in the enum.


1 Answers

Instead of using a computed property, you could use a mapped type:

interface ApiTerm {
    term: {
        name: string,
        value: string,
    }
}

enum Operation {
    And = "and",
    Or = "or",
}

type ApiBinary<O extends Operation> = {[o in O]: {
        lhs: ApiItem,
        rhs: ApiItem,
    }
}

type ApiItem =
    | ApiTerm
    | ApiBinary<Operation.And>
    | ApiBinary<Operation.Or>
    ;

const andExample: ApiBinary<Operation.And> = {
    'and': {
        lhs: { term: { name: 'a', value: 'b' } },
        rhs: { term: { name: 'c', value: 'd' } }
    }
}

However, note that there is no way to express the restriction that ApiBinary can have only one property; for example, someone could declare type N = ApiBinary<Operation.And | Operation.Or>;

When you use a mapped type, it does not mix well with computed properties - the compiler can't infer that the computed property type conforms to the constraint in ApiItem in this example:

const foo = ({ kind, lhs, rhs }: { kind: Operation, lhs: ApiItem, rhs: ApiItem }): ApiItem =>
({ [kind]: { lhs, rhs } });

The error says is not assignable to type 'ApiBinary<Operation.Or>' because the compiler tries to check assignability to all union members and if it fails it only tells about the last one.

The only way I can think of writing this function is long-winded:

const foo = <O extends Operation>({ kind, lhs, rhs }: { kind: O, lhs: ApiItem, rhs: ApiItem }) => {
    const result = {} as ApiBinary<O>;
    result[kind] = {lhs, rhs};
    return result;
}
like image 146
artem Avatar answered Oct 12 '22 06:10

artem