Home > front end >  Typescript: generic interface for creating object containing options and a selection
Typescript: generic interface for creating object containing options and a selection

Time:08-28

I am trying to create a generic option type/interface which will be used to create specific option types. This generic type/interface takes a set of options as input. The parameter value contains the current selection, and options contains all the options. The parameter value should only be allowed to hold strings contained in options and options should be defined from the list of options defined externally.

This is my current progress:

export type GenericListOfOptions<list> = {
    type: 'list',
    name: string,
    value: list,
    options: Array<list>, // I am not sure if this is right...
}

// Here i define my options:
const these_options = ['yes', 'no', 'maybe'] as const;

// Here i define a specific option type from the
// generic option type. This specific option type is
// defined from the 'these_options' constant.
type MySpecificOptions = GenericListOfOptions<typeof these_options[number]>

// Here i use the type to create an option object.
let my_options: MySpecificOptions = {
    type: 'list',
    name: 'am i going to the gym?',
    value: 'no',
    options: these_options, // Here is an error relating readonly properties...
    // 'options' should be equal to ['yes', 'no', 'maybe'].
    // I preferrably want to reuse the 'these_options' array to define the 'options' parameter.
}

The error given is:

The type 'readonly ["yes", "no", "maybe"]' is 'readonly' and cannot be assigned to the mutable type '("yes" | "no" | "maybe")[]'.ts(4104)
interfaces.ts(137, 5): The expected type comes from property 'options' which is declared here on type 'MySpecificOptions '

But maybe there are other more general errors of this idea, so any help would be greatly appreciated!

CodePudding user response:

When you initialized these_options as

const these_options = ['yes', 'no', 'maybe'] as const;
// const these_options: readonly ["yes", "no", "maybe"]

you used a const assertion, presumably so the compiler would remember that the elements consist of the string literal types "yes", "no", and "maybe". But it also has the effect of making these_options have a readonly tuple type, which is a subtype of the ReadonlyArray type. It is not known to have the mutating methods of the Array type, like the push() method or the splice() method, etc. ReadonlyArray<T> is therefore wider than Array<T>. That means Array<T> is assignable to ReadonlyArray<T> but not vice versa.


So given the definition of GenericListOfOptions<T> as

type GenericListOfOptions<T> = {
    type: 'list',
    name: string,
    value: T,
    options: Array<T>, 
}

then of course this will be a problem:

let my_options: MySpecificOptions = {
    type: 'list',
    name: 'am i going to the gym?',
    value: 'no',
    options: these_options // error
}

because the options property must be an Array<"yes" | "no" | "maybe">, but these_options is only known to be a ReadonlyArray<"yes" | "no" | "maybe">.


Presumably you don't actually intend to mutate the contents of the options property of GenericListOfOptions<T>. If so, then the simplest solution is to widen the options property type to ReadonlyArray<T>:

type GenericListOfOptions<T> = {
    type: 'list',
    name: string,
    value: T,
    options: ReadonlyArray<T>, // change
}

let my_options: MySpecificOptions = {
    type: 'list',
    name: 'am i going to the gym?',
    value: 'no',
    options: these_options // okay
}

On the other hand, if you do want to mutate these_options, and you do need Array<T> instead of Readonly<T>, then you could do an array spread to copy the const-asserted tuple into a mutable array:

const these_options = [...['yes', 'no', 'maybe'] as const];
// const these_options: ("yes" | "no" | "maybe")[]

And then you could leave GenericListOfOptions<T> alone.

Playground link to code

  • Related