Home > Net >  ngFor converts string literal type to string, throws "Element implicitly has an 'any'
ngFor converts string literal type to string, throws "Element implicitly has an 'any'

Time:11-06

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'. -->

CodePudding user response:

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>

CodePudding user response:

Self-answering because I have just found a solution that works for me. The workaround by Tobias S. works, but it requires defining a new function. One can instead iterate over the constant Fruits:

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 {
    fruits = Fruits;
    fruitBasket: FruitCollection = {
        apple: 10,
        banana: 10
    }
    fruitEaten: FruitCollection = {
        apple: 0,
        banana: 0
    }
    constructor() { }
    ngOnInit(): void { }
    eatFruit(fruit: Fruit) {
        this.fruitEaten[fruit]  ;
        this.fruitBasket[fruit]--;
    }
}

I don't understand why, but iterating over fruits = Fruits = ["apple", "banana"] instead of over fruitBasket[] preserves the string literal union type inside *ngFor, preventing any type errors.

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

So I am not sure why this works, but it does.

  • Related