Skydidi
Skydidi

Reputation: 21

Use a result of string+string as the key of object in Typescript

I have a series of keys as CONCEPT1 to CONCEPT6, so I try to use string+number to go through get all the value:

for (let i of sampleData){
  for (let j=1; j<7; j++){
     let tmp:string = 'CONCEPT'+j.toString();
     console.log(i[tmp])
  }
}

But I got the error TS7053

No index signature with a parameter of type 'string' was found on type '{ "CONCEPT1": string;...'

Upvotes: 0

Views: 258

Answers (3)

jcalz
jcalz

Reputation: 327744

The other answers here are making the assumption that sampleData can be treated like an array of string-indexable objects. This is probably not true, or at least, the code would fail to warn you in cases like this:

const sampleDataWidened: Array<{[k: string]: string}> = [{ CONCEPT1: "A" ... }]; 

for (let i of sampleDataWidened) {
    for (let j = 1; j < 7; j++) {
        let tmp: string = 'COCNEPT' + j.toString();
        console.log(i[tmp]); // no error now
    }
}

Everything looks fine but I misspelled 'CONCEPT' above and bad things will happen at runtime if you expect i[tmp] not to be undefined.

If you really need to iterate through numbers and concatenate them to strings to get your keys, TypeScript 4.0 and below really has no way to guarantee safety here; it just can't see that "CONCEPT" + 1 will produce "CONCEPT1" and check whether that is or is not a key of the elements of sampleData. Concatenating string with number produces just string, not a string literal type. In this case, you could just tell the compiler that you know what you're doing. I'd suggest using a type assertion:

for (let i of sampleData) {
    for (let j = 1; j < 7; j++) {
        let tmp: string = 'CONCEPT' + j.toString();
        console.log(i[tmp as keyof typeof sampleData[number]]); // assert
    }
}

Here you're telling the compiler: "Look, tmp can be treated as one of the keys of each element of sampleData. Trust me." And the compiler does trust you and everything is fine. Of course you can still misspell "CONCEPT" or use values of j that are outside the range of actual keys and the compiler won't stop it. It can't. In some sense this is the same answer as making sampleData an array of string-indexable objects, but at least the assertion's scope is limited to your loop and doesn't pollute the type of sampleData outside of that loop.


The only safe thing to do in pre-TS-4.1 is to not concatenate strings, but use a hardcoded tuple of string literals:

for (let i of sampleData) {
    for (let tmp of ['CONCEPT1', 'CONCEPT2', 'CONCEPT3', 
     'CONCEPT4', 'CONCEPT5', 'CONCEPT6'] as const) {
        console.log(i[tmp]);
    }
}

Here tmp iterates over a tuple of literals (via a const assertion to ask the compiler to pay attention to the literal values in that array).


Now, starting in TypeScript 4.1 you will be able to represent the results of string concatenation by using template literal types as implemented in microsoft/TypeScript#40336. The compiler still won't infer that "CONCEPT" + 1 is of type "CONCEPT1" (that would be covered by something like microsoft/TypeScript#13969), but you can at least write a function that works this way:

type Templable = string | number | bigint | boolean;
const concat = <T extends Templable, U extends Templable>(
    t: T, u: U
) => String(t) + String(u) as `${T}${U}`;

const concept1 = concat("CONCEPT", 1);
// const concept1: "CONCEPT1"

Armed with that, you could rewrite your loop to use it. Well, unfortunately, the compiler also doesn't realize from the loop that j will be of type 1 | 2 | 3 | 4 | 5 | 6. It infers number. Instead of doing that, let's iterate over [1, 2, 3, 4, 5, 6] as const (using a const assertion again):

for (let i of sampleData) {
    for (let j of [1, 2, 3, 4, 5, 6] as const) {
        let tmp = concat('CONCEPT', j)
        console.log(i[tmp])
    }
}

Now there's no error at all. The compiler sees tmp as type

let tmp: "CONCEPT1" | "CONCEPT2" | "CONCEPT3" | "CONCEPT4" | "CONCEPT5" | "CONCEPT6"

and it knows that those are all keys of the only element of sampleData, and it works. It would definitely catch a misspelling too:

for (let i of sampleData) {
    for (let j of [1, 2, 3, 4, 5, 6] as const) {
        let tmp = concat('COCNEPT', j)
        console.log(i[tmp]) // error!
        // ---------> ~~~
        // Property 'COCNEPT1' does not exist on type 
        // '{ CONCEPT1: string; CONCEPT2: string; CONCEPT3: string; 
        //    CONCEPT4: string; CONCEPT5: string; CONCEPT6: string; }'. 
        // Did you mean 'CONCEPT1'?
    }
}

That's a great error, especially the suggestion.


I wouldn't say that you really need to use something like template literal types here, even when TS4.1 comes out. In many cases it's fine for you to accept a little risk and use a type assertion.

Playground link to code

Upvotes: 1

Pritam Kadam
Pritam Kadam

Reputation: 2527

When you define sample data like below:

const sampleData = [
  { CONCEPT1: 'A', CONCEPT2: 'B', CONCEPT3: 'C' }
]

Note that here you have not specified type sampelData const, in this case compiler infers very specific type which is this:

const sampleData: {
    CONCEPT1: string;
    CONCEPT2: string;
    CONCEPT3: string;
}[]

If you look at it closely, keys are not any strings but concrete singletons, these are called literal types.

So when you try to access value from this const using string index, ts compiler gives you correct error which is this

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ CONCEPT1: string; CONCEPT2: string; CONCEPT3: string; }'

Simple fix in your case is to provide explicit Record<string, string>[] type to sampleData in your

Following code prints A to F

const sampleData: Record<string, string>[] = [
  { CONCEPT1: 'A', CONCEPT2: 'B', CONCEPT3: 'C', CONCEPT4: 'D', CONCEPT5: 'E', CONCEPT6: 'F' }
]

for (const i of sampleData) {
  console.log(i)

  for (let j = 1; j < 7; j++) {
    const tmp: string = 'CONCEPT' + j.toString()
    console.log(i[tmp])
  }
}

/* Output
 A
 B
 C
 D
 E
 F
*/

Upvotes: 0

Kamen Minkov
Kamen Minkov

Reputation: 3712

You might set the type of sampleData more explicitly and it should work:

type Data = {
    [key: string]: string;
};

let sampleData: Array<Data> = [{
    "CONCEPT1": "A",
    "CONCEPT2": "B",
    "CONCEPT3": "C"
}];

for (const i of sampleData) {
    for (let j = 0; j < 7; j++) {
        const tmp: string = 'CONCEPT' + j.toString();

        console.log(i[tmp]);
    }
}

Upvotes: 0

Related Questions