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.