0

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?

10
  • I don't understand why the open-ended tuple solution doesn't work for you (i.e., the one with ...EnforcedValue[] at the end). Closed-ended tuples like [EnforcedValue.FirstValue] have a length restriction, so you can't "extend" it by adding stuff to the end. Conversely, open-ended tuples allow you to "put whatever values at the end of the array" which you do want to allow, or else ExtendedInterface wouldn't be assignable to Interface. It's very unclear to me what you mean by "extend". Please edit to clarify your use cases to make sense; right now it's contradictory. Commented Mar 18 at 17:01
  • Well the thing is that down the line I want to check what values are in the values property and if I keep it open ended, someone could put [EnforcedValue.FirstValue, EnforcedValue.SecondValue] even into an object of type Interface, which should not happen. I want to be able to define contents of values as strictly as possible, but also be able to use inheritance, where the subtypes would add something to the values property. Commented Mar 19 at 19:59
  • Not sure how would I clarify my question more. Basically what I am trying to do is to create a runtime type checking system. Imagine many objects of many types get shuffled and since they are plain objects and not instances of any class, I cannot use instanceof to find out which is which. That is why I want to very strictly define the values, which I can then use to find out which type of object I am working with. Commented Mar 19 at 20:30
  • That is why any interface extending an existing interface should contain all values coming from sub interfaces plus one more for the new interface to make it work like usual inheritance. And that is why it is desirable that nobody can freely append stuff at the end of the array for the given type. Commented Mar 19 at 20:43
  • You're asking for contradictory things. You do want someone to come along and put [EnforcedValue.FirstValue, EnforcedValue.SecondValue] for an object of type Interface, because if that's prevented then an ExtendedInterface would not be an Interface. Please let me know whether or not you understand this fundamental feature of TypeScript's structural type system. Commented Mar 19 at 20:53

1 Answer 1

1

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

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

7 Comments

In this case, ObjectWithEnforcedValues<T>, Interface<T>, and ExtendedInterface<T> are all identical types. The only difference happens if you give them different T arguments, which you apparently do with defaults? This isn't really generic in any useful way. The fact remains that ExtendedInterface extends Interface is not true here, even though ExtendedInterface<T> extends Interface<T> is trivially true. It's not clear to me how this helps OP, but I guess it's not clear to me what OP wants, so 🤷‍♂️
I used the TS playground you provided and unfortunately this does not work. i2 being an object of type ExtendedInterface cannot be used in a function which accepts Interface
@Tom you could use an object instead of an array, added to the answer
Hmmm, that is very interesting. I will try and play around with that a little bit, but this is looking really promising. It has a few downsides like a little too complex definition of interfaces and the necessity of some random value like true you used, but if it worked that would be nice.
Maybe it would also be good to include the solution here in the answer and not just in the playground, but anyway thanks
|

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.