Home > Enterprise >  Extend interface with dynamic keys?
Extend interface with dynamic keys?

Time:12-29

I have an interface that allows use an object with dynamic keys with nested objects:

interface simpleObject
{
  [key: string]: string|number|simpleObject|Array<string|number|simpleObject>
}

const myObject:simpleObject = {};
myObject.blah = "this works";
myObject.data = {works: "yes"};
myObject.data.val = 123;
myObject.array = [{nested: "object"}, 1, 2, 3];

How can I extend it to also allow boolean ?

I've tried something like this:

interface simpleObjectExtended extends simpleObject
{
  [key: string]: boolean;
}

but it gives error:

Interface 'simpleObjectExtended' incorrectly extends interface 'simpleObject'

CodePudding user response:

Here's a way to do it which derives the types from the original interface using utilities:

TS Playground

interface SimpleObject {
  [key: string]: string | number | SimpleObject | Array<string | number | SimpleObject>;
}

interface SimpleObjectExtended extends Omit<SimpleObject, string> { // omit the existing type
  [key: string]: ( // then rebuild from the existing type:
    Exclude<SimpleObject[string], any[] | SimpleObject> // the original non-array and non-recursive-types
    | boolean // boolean
    | SimpleObjectExtended // this interface
    | Array< // an array of:
        Exclude< // the original non-recursive types in the array
          Extract<SimpleObject[string], any[]>[number],
          SimpleObject
        >
        | boolean // boolean
        | SimpleObjectExtended // this interface
      >
  );
}

declare const s: SimpleObjectExtended;
s.example; // string | number | boolean | SimpleObjectExtended | (string | number | boolean | SimpleObjectExtended)[]
s.bool = true; // ok
s.bool; // true
s.o = {}; // ok
s.o; // SimpleObjectExtended 
s.o.bool = false; // ok
s.o.bool; // false

That said, I'd find this approach more maintainable:

TS Playground

type Core<T> = T | string | number | Array<T | string | number>;

type Base = {
  [key: string]: Core<Base>;
};

type BaseWithBoolean = {
  [key: string]: Core<BaseWithBoolean | boolean>;
};

CodePudding user response:

It makes sense that you cannot declare the index type as just boolean in the child class, because it must support at least the same values that the parent supports in order to be a valid subtype. To include boolean as one of the possible values, you can add it to the original signature. Note that the references to itself must also be updated to name the child class in order to support booleans in nested values.

interface Simple {
  [key: string]: string | number | Simple | Array<string | number | Simple>;
}

interface SimpleWithBoolean extends Simple {
  [key: string]: string | number | boolean | SimpleWithBoolean | Array<string | number | boolean | SimpleWithBoolean>;
}

const simpleWithBoolean: SimpleWithBoolean = {};
simpleWithBoolean.stringProp = "stringProp";
simpleWithBoolean.numberProp = 123;
simpleWithBoolean.booleanProp = true;
simpleWithBoolean.simpleProp = {};
simpleWithBoolean.array = [{nested: "object", booleanProp: true, }, 1, 2, 3, true];

The downside of this solution is that if Simple is updated, SimpleWithBoolean must also be updated to match.

I believe this construction will achieve what you want without needing to keep the two definitions in sync:

interface Simple {
  [key: string]: string | number | Simple | Array<string | number | Simple>;
}

type Extend<Original, Union, Addition> = Union extends Array<infer Inner> ? Array<Inner extends Original ? never : Inner | Addition> : Union extends Original ? never : Union | Addition;

type ExtendEach<Original, Addition> = {
  [P in keyof Original]: Extend<Original, Original[P], Addition>;
};

type NonRecursiveSimpleWithBoolean = ExtendEach<Simple, boolean>;
type SimpleWithBoolean = ExtendEach<NonRecursiveSimpleWithBoolean, NonRecursiveSimpleWithBoolean>;

const simpleWithBoolean: SimpleWithBoolean = {};
simpleWithBoolean.stringProp = "stringProp";
simpleWithBoolean.numberProp = 123;
simpleWithBoolean.booleanProp = true;
simpleWithBoolean.simpleProp = {};
simpleWithBoolean.array = [{nested: "object", booleanProp: true, }, 1, 2, 3, true];

TS Playground

I'm not totally happy with this, as it requires an intermediate step to create the new type without Simple as a union member and then a final step to make the new type recursive. I don't know if it's possible to refer to the type being created in a mapped type's definition. If anyone knows how to do this in one step without NonRecursiveSimpleWithBoolean, I'd like to know!

  • Related