How to type "recursive object of primitives"?

142 views Asked by At

I'm trying to come up with an interface that will behave similarly to any in that properties can be accessed with the dot notation object.foo.bar and will describe "recursive object of primitives" that

  1. can only have keys of type string
  2. can only have values of type Primitive | Primitive[]

Where Primitive can be defined as type Primitive = string | boolean | number and by "nested object" I mean that the value can also be another recursive object of primitives.

So far I've come up with the following:

type Primitive = string | number | boolean

interface IPrimitveObject extends Record<string, IPrimitveObject | IPrimitveObject[] | Primitive | Primitive[]> {}

But this fails right on the second level of property access.

const testObject: IPrimitveObject = {
    foo: 'bar',
    bar: {
        baz: true
    }
}

testObject.bar.baz //compiler error: "Property 'bar' does not exist on type 'string | number | boolean | IPrimitveObject | IPrimitveObject[] | Primitive[]'."

Which makes sense. But how can one overcome this?

1

There are 1 answers

2
jcalz On BEST ANSWER

Once you annotate a variable as type IPrimitiveObject, that's it's type, and the compiler won't keep track of any particular properties on it that are not known in the definition of IPrimitiveObject.

If you want to create an object literal and have the compiler check that it is assignable to IPrimitiveObject without actually widening its type to IPrimitiveObject and subsequently forgetting all of its specific properties, you can make a generic helper function which only accepts arguments assignable to IPrimitiveObject, and returns them unchanged:

const asIPrimitiveObject = <T extends IPrimitveObject>(t: T) => t;

And you'd use it instead of annotating:

const testObject = asIPrimitiveObject({
  foo: 'bar',
  bar: {
    baz: true
  }
});
/* const testObject: {
    foo: string;
    bar: {
        baz: true;
    };
} */

testObject.bar.baz; // type true

And if you use something incompatible you should get an error:

const badObject = asIPrimitiveObject({
  foo: "bar", // okay
  bar: { baz: { qux: () => 1 } } // error!
//~~~ <-- Type '{ baz: { qux: () => number; }; }' is not assignable to
 //type 'string | number | boolean | IPrimitveObject | IPrimitveObject[] | Primitive[]'.
});

Okay, hope that helps; good luck!

Playground link to code