Skip to content
Advertisement

Typescript: Define for JSON nested object

I have a nested json object and define a interface for it

interface Menu {
    [key: string]: string[] | Menu;
}

const menuOnlyForRoles: Menu = {
    site: {
        only: [],
        category: {
            only: ['manager', 'head', 'lead'],
        },
        'floor-section': {
            only: ['head', 'lead'],
        },
    },
};

But when I use it. It appear a warning.

const menuPermissionForKey = menuOnlyForRoles.site || { only: [] };
const rolesCanAccessMenu= menuPermissionForKey.only || [];
                                               ▔▔▔▔
     any
     Property 'only' does not exist on type 'Menu | string[]'.
       Property 'only' does not exist on type 'string[]'

What did I do wrong or missing?

Advertisement

Answer

You didn’t do anything wrong per se. TypeScript just cannot infer whether your menuPermissionForKey is a Menu object or a string array.

Ideally, you’d define your Menu type more strictly. Barring that, you can create a type predicate:

interface Menu {
    [key: string]: string[] | Menu;
}

const menuOnlyForRoles: Menu = {
    site: {
        only: [],
        category: {
            only: ['manager', 'head', 'lead'],
        },
        'floor-section': {
            only: ['head', 'lead'],
        },
    },
};

function isMenu(data: Menu | string[]): data is Menu {
    return !Array.isArray(data);
}

const menuPermissionForKey = menuOnlyForRoles.site || { only: [] };
const rolesCanAccessMenu= isMenu(menuPermissionForKey)
                              ? menuPermissionForKey.only || []
                              : [];

Playground link


Alternatively, if you’re dealing with static data that is known at compile-time, you can use a const assertion, optionally with the satisfies operator to enforce type-checking at edit time:

interface Menu {
    [key: string]: readonly string[] | Menu;
}

const menuOnlyForRoles = {
    site: {
        only: [],
        category: {
            only: ['manager', 'head', 'lead'],
        },
        'floor-section': {
            only: ['head', 'lead'],
        },
    },
} as const satisfies Menu;

const menuPermissionForKey = menuOnlyForRoles.site || { only: [] };
const rolesCanAccessMenu = menuPermissionForKey.only || [];

Playground link

Advertisement