typescriptinheritanceinterfacetuples

How to extend typescript interface with specific values enforced in an array property


I have a basic interface, which I use down the line to enforce specific values on other interfaces:

interface ObjectWithEnforcedValues {
    values: readonly EnforcedValue[];
}

enum EnforcedValue {
    FirstValue = 'FirstValue',
    SecondValue = 'SecondValue',
}

Then I use it on an actual interfaces I want to use to create actual objects, for example:

interface Interface extends ObjectWithEnforcedValues {
    values: readonly [EnforcedValue.FirstValue];
}

So far so good, but the problem comes when I try to extend the Interface and increase the number of enforced values. Typescript does not like this at all:

interface ExtendedInterface extends Interface {
    values: readonly [EnforcedValue.FirstValue, EnforcedValue.SecondValue]
}

Basically the goal is to make sure anyone creating new instances of objects using these interfaces sets the values property to the specific enforced values, but I am not able to make it work properly with inheritance. Logically it seems that it SHOULD work, because in the case above, Interface enforces the first value in the array to be FirstValue and ExtendedInterface adheres to that and only adds a SecondValue to the second position of the array. Is there any way to do this or Typescript just does not allow this? First thing that comes to mind is using class and just set the property directly there instead of using interfaces like this, but unfortunately I cannot use that, everything is just plain objects.

Things I have tried:

  1. Defining ExtendedInterface using extends Omit<Interface, 'values'> and redefining values - This defies proper inheritance and disallows passing ExtendedInterface instances as a parameter to functions accepting Interface
  2. Defining interfaces in question like this:
interface Interface extends ObjectWithEnforcedValues {
   values: readonly [EnforcedValue.FirstValue, ...EnforcedValue[]];
}
interface ExtendedInterface extends Interface {
   values: readonly [EnforcedValue.FirstValue, EnforcedValue.SecondValue, 
                     ...EnforcedValue[]]
}

This makes more or less everything work as I need, but allows to put whatever values at the end of the array, which I do not want to allow.

Is there any way to make it work properly or should I just give up and use the second option?


Solution

  • You could use generics:

    Playground

    interface ObjectWithEnforcedValues<T extends readonly EnforcedValue[] = readonly EnforcedValue[]> {
        values: T
    }
    
    enum EnforcedValue {
        FirstValue = 'FirstValue',
        SecondValue = 'SecondValue',
    }
    
    
    interface Interface<T extends readonly EnforcedValue[] = readonly [EnforcedValue.FirstValue]> extends ObjectWithEnforcedValues <T> {
    
    }
    
    
    interface ExtendedInterface<T extends readonly EnforcedValue[] = readonly [EnforcedValue.FirstValue, EnforcedValue.SecondValue]> extends Interface<T> {
        
    }
    
    
    const i1: Interface = {values: [EnforcedValue.FirstValue]}
    const i2: ExtendedInterface = {values: [EnforcedValue.FirstValue, EnforcedValue.SecondValue]}
    

    To ensure structural inheritance you could use an object instead of an array:

    Playground