Search code examples
javascripttypescriptfor-loop

Typescript string index in foreach loop


I am building a tool to check statuses on the network. The environment file has a bunch of addresses to various items, like VMs, DBs, etc. The type of the env looks like this:

export type EnvDefinition = {
  'DetailedErrors': boolean,
  'Logging': LogDefinition,
  'SystemElementGroups': {
    'VMs': SystemElement[],
    'Applications': SystemElement[],
    'Databases': SystemElement[],
  },
}

SytemElement is just another type that defines the IP address and whatever else is needed.

When I load the env data from the json file, I cast it like this:

import envData from '../development.json';
const data: EnvDefinition = envData;

Then I try to access the individual group data like this:

const assetTypes = ['VMs', 'Applications', 'Databases'];
for (const assetType of assetTypes) {
    const connectStrings = data.SystemElementGroups[assetType];

No matter what I try I keep getting a TS error: TS7053: Element implicitly has an any type because expression of type string can't be used to index type

How can I define this so I can access the SystemElementGroups in a loop?

Thanks


Solution

  • In the line

    const assetTypes = ['VMs', 'Applications', 'Databases'];
    

    you haven't annotated the type like const assertTypes: XXX =. So TypeScript has to infer the type of assetTypes from the array literal with which you're initializing it. And the type that it infers will be consulted in all subsequent uses of the assetTypes variable.

    There are lots of possible types that are applicable for the particular array literal. It could be something super general like the unknown type, or it could be very specific like a tuple type consisting of the string literal types ["VMs", "Applications", "Databases"]. Or anything in between, like Array<string | number> | boolean. It chooses based on heuristic rules that work well in a wide variety of real-world code, but these rules don't always do what everyone wants.

    In the case of an array literal with string literals inside it, TypeScript infers the type to be an array of string: that is, Array<string> or the equivalent string[]. That allows you to push() new string values onto it, or sort() it, etc. But it means the compiler does not even try to track which string values are in the array or where they are. Again, this works well for many use cases.

    But unfortunately that means TypeScript has no idea that the code

    for (const assetType of assetTypes) {
        const connectStrings = data.SystemElementGroups[assetType];
    

    is safe. You are indexing into data.SystemElementGroups with assetType of type string, which could be any string at all, most of which are not known to be keys. And that's what the error message says.

    The problem is that assetTypes has too general of a type to be useful for your needs.


    If you want to convince the compiler that assetTypes only contains the keys of the SystemElementGroups property of an EnvDefinition, you could annotate assertTypes as such:

    const assetTypes: Array<keyof EnvDefinition['SystemElementGroups']> =
      ['VMs', 'Applications', 'Databases'];
    for (const assetType of assetTypes) {
      const connectStrings = data.SystemElementGroups[assetType]; // okay
    }
    

    That works, but it's a little verbose.

    Another approach is use a const assertion to tell the compiler that the array initialized by the array literal is not intended to change at all. It will forever be an array of three elements, whose types are the string literal types of the initializing elements, and they will be in the exact order as initialized:

    const assetTypes = ['VMs', 'Applications', 'Databases'] as const;
    // const assetTypes: readonly ["VMs", "Applications", "Databases"]
    

    Now the type of assetTypes is very specific. You can't push() some random string onto it, or sort() it, or write assetTypes[0]="random". This might be even more specific than you care about, but it's easy to do with as const... and now the compiler has enough information to know that each element of assetTypes is a valid key for data.SystemElementGroups:

    for (const assetType of assetTypes) {
      const connectStrings = data.SystemElementGroups[assetType]; // okay
    }
    

    Playground link to code