Search code examples
angulartypescripttemplatesngforstring-literals

ngFor converts string literal type to string, throws "Element implicitly has an 'any' type because expression of type 'string' can't be used...."


Using the iterator *ngFor converts a string union literal type ("apple" | "banana") to a string type. When I use it as an index of an array expecting the correct string union literal type I get the error:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'FruitCollection'.

apple-banana-component.ts:

import { Component, OnInit } from '@angular/core';

const Fruits = ["apple", "banana"] as const;
type Fruit = typeof Fruits[number]; // "apple" | "banana"
type FruitCollection = { [fruit in Fruit]: number }; // {apple: number, banana: number}

@Component({
    selector: 'app-apple-banana',
    templateUrl: './apple-banana.component.html'
})
export class AppleBananaComponent implements OnInit {
    fruitBasket: FruitCollection = {
        apple: 10,
        banana: 10
    }
    fruitEaten: FruitCollection = {
        apple: 0,
        banana: 0
    }
    constructor() { }
    ngOnInit(): void { }
    eatFruit(fruit: Fruit) {
        this.fruitEaten[fruit]++;
        this.fruitBasket[fruit]--;
    }
}

apple-banana-component.html:

<div>
You have eaten {{fruitEaten['apple']}} apples and {{fruitEaten['banana']}} bananas. <!-- works -->

<div *ngFor="let fruit of fruitBasket | keyvalue">
    {{fruit.key}}: 
    {{fruit.value}} in basket, 
    {{fruitEaten[fruit.key]}}  <!-- ERROR: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'FruitCollection'. -->    
    eaten.
    <button (click)="eatFruit($any(fruit.key))">eat {{fruit.key}}</button>
</div>
</div>

For some reason I can't comprehend, $any(fruit.key) works inside eatFruit() but not inside fruitBasket[].

{{fruitEaten[fruit.key as Fruit]}} <!-- ERROR: Parser Error: Missing expected ] at column 22 [...] -->

{{fruitEaten[fruit.key as keyof typeof fruitEaten]}} <!-- ERROR: Parser Error: Missing expected ] at column 22 [...] -->

{{fruitEaten[$any(fruit.key)]}} <!-- ERROR: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'FruitCollection'. -->

Solution

  • The problem is that the key produced by the keyvalue is always a string. You can't use a string to index your type but you also can't use type assertions in a template. The $any was a good idea, but even any is not allowed to index object types without index signatures when noImplicitAny is set to true.

    You could create a function in your component to do the casting.

    apple-banana-component.ts

    public isFruit(value: string): Fruit {
      return value as Fruit
    }
    

    You can now use this function in your template.

    <div *ngFor="let fruit of fruitBasket | keyvalue">
        {{fruit.key}}: 
        {{fruit.value}} in basket, 
        {{fruitEaten[isFruit(fruit.key)]}}
        eaten.
        <button (click)="eatFruit(isFruit(fruit.key))">eat {{fruit.key}}</button>
    </div>