Home > front end >  How to write chained dynamic object properties in Typescript?
How to write chained dynamic object properties in Typescript?

Time:10-17

I am switching a JS project to TS and am struggling with a certain part of my code, which TS complains about and I don't know how to resolve the issue.

In TS, I want to collect data in a dynamic object, called licensesStats, that in the end might look like this:

{
  'DYNAMIC_NAME': {
    'Usage count': 2,
    'Used by': {
      'SOME_DYNAMIC_NAME': null,
      'SOME_OTHER_NAME': null,
      [...]
    }
  },
  [...]
}

For my licensesStats object, I have created the following interface:

interface LicenseStatsCounter {
    [key: string]: number;
}

interface LicenseStatsModule {
    [key: string]: null;
}

interface LicenseStatsModules {
    [key: string]: LicenseStatsModule;
}

interface LicensesStats {
    [key: string]:
        | LicenseStatsCounter
        | LicenseStatsModules;
}

licensesStats: LicensesStats = {}
...

The keys "Usage count" and "Used by" are stored in variables, and so in the code during data collection, there is a command that says:

licensesStats[license][titleUsedBy][moduleNameAndVersion] = null;

But TS complains:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'number | LicenseStatsModule'.
No index signature with a parameter of type 'string' was found on type 'number | LicenseStatsModule'.

What do I have to do in order to satisfying TypeScript's needs?

P.S.: Here is an example TS file for my issue:

const titleUsageCount = 'Usage count';
const titleUsedBy = 'Used by';

interface LicenseStatsCounter {
    [key: string]: number;
}

interface LicenseStatsModule {
    [key: string]: null;
}

interface LicenseStatsModules {
    [key: string]: LicenseStatsModule;
}

interface LicensesStats {
    [key: string]: LicenseStatsCounter | LicenseStatsModules;
}

// Exported functions:
export const readLicenses = (argv: Argv) => {
    const licensesStats: LicensesStats = {};

    const license = 'MIT';
    const moduleNameAndVersion = '[email protected]';
    licensesStats[license] = licensesStats[license] || {
        [titleUsageCount]: 0,
        ...((argv.summaryPlus as boolean) && { [titleUsedBy]: {} }),
    };
    (licensesStats[license][titleUsageCount] as number)  ;

    // This is the problematic line, where TS says:
    // Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'number | LicenseStatsModule'.
    // No index signature with a parameter of type 'string' was found on type 'number | LicenseStatsModule'.
    licensesStats[license][titleUsedBy][moduleNameAndVersion] = null;
    }
};
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

With your current setup, typescript reaches licensesStats[license][titleUsedBy] and sees two possible values: either a number or an object (the LicenseStatsModule) and throws an error because a number can't contain a property.

You're going to need some kind of conditional checks to make typescript happy, something like:

const x = licensesStats[license][titleUsedBy];

if(typeof x !== "number"){
  const y = x[moduleNameAndVersion];

  // ... do something
  return;
}

// ... do something
return;
  • Related