0

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'

1
  • 1
    Looking at what you've shared and knowing nothing more, it seems like you're on the path of trying to create a type to represent [a subset of] valid, parsed JSON, so I'd like to share this relevant GitHub issue link: ms/TS#1897 Commented Dec 29, 2021 at 6:48

2 Answers 2

2

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>;
};
Sign up to request clarification or add additional context in comments.

Comments

1

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!

2 Comments

This doesn't look like extending, it's simply manually recreating/copying...not quiet what I'm after. But thanks anyway.
I edited my answer to include a generic solution. Perhaps that will help you!

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.