1

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?

0

1 Answer 1

4

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

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

2 Comments

Thanks for the answer! This nicely solves the case of creating/assigning the object literal. What if one gets the objects from some IO (think JSON.parse()) and wants to annotate it with the IPrimitiveObject type so that access to the object is restricted to primitive values. Like parsedObject.bar is ok but parsedObject.bar() would rise an error since its properties can not be a function.
Have you tried it? Seems to work for me

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.