Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular's KeyValuePipe loses its types

When I'm using the Angular's KeyValuePipe in my template, the key pipe-results is losing its type. Here is an example.

app.component.ts:

export class AppComponent {
  someObject: Record<`${'key1' | 'key2'}`, {myCustomValue: string}> = 
              {key1: {myCustomValue: 'value1'}, key2:{myCustomValue:'value2'}}

  getKeyValueElement(key: 'key1' | 'key2', value: {myCustomValue: string}){
    console.log(key, value)
  }
}

Then in my app.component.html:

<div *ngFor="let keyValueElement of someObject | keyvalue"
  (click)="getKeyValueElement(keyValueElement.key, keyValueElement.value)">
</div>

And here is the error I get:

enter image description here

The app refuses to compile since there is incompatibility between the getKeyValueElement key parameter - which is 'key1' | 'key2', and the key from the pipe-result which is string.

Now, there are 2 different ways that I know how to solve it:

  1. To use $any() in the template to avoid type checking - Not a recommended practice.
  2. To pass a compareFn to the KeyValuePipe with the exact requested types in the signature. this will effect the types rendered in the key&value pipe-results. This is also not a good solution in most cases when you don't really need a custom compareFn.

So, I'm looking for another solution. In my eyes, this error should not appear in the first place.

like image 406
Avraham Weinstein Avatar asked Jul 17 '26 02:07

Avraham Weinstein


2 Answers

That because of the signature of transformof the keyvalue pipe.

One of the entries is : transform<K extends number, V>( input: Record<K, V>, compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number): Array<KeyValue<string, V>>;

It matches type Record<K extends string | number | symbol, T> = { [P in K]: T; }

You can see Record has number as key.

So the solution would be to use a proper map :

const someObject: Map<('key1' | 'key2'), { myCustomValue: string }> =
    new Map([['key1', { myCustomValue: 'value1' }], ['key2', { myCustomValue: 'value2' }]])
like image 87
Matthieu Riegler Avatar answered Jul 19 '26 16:07

Matthieu Riegler


I created the following pipe which uses the existing pipe logic but does not convert the keys to strings but instead retains the original types of the keys.

import { KeyValue, KeyValuePipe } from "@angular/common";
import { Pipe, PipeTransform, ɵdefaultKeyValueDiffers } from "@angular/core";

@Pipe({
  name: 'KeyValue',
  standalone: true,
})
export class KeyValueTypedPipe implements PipeTransform {

  keyValuePipe = new KeyValuePipe(ɵdefaultKeyValueDiffers);

  transform<V extends Record<string, unknown>>(
    input: V,
    compareFn = (_a: V, _b: V) => 0, // Bonus: retain order of keys
  ) {
    return this.keyValuePipe.transform(
      input, 
      compareFn as unknown as Parameters<typeof this.keyValuePipe.transform>[1]
    ) as Array<KeyValue<keyof V, V[keyof V]>>;
  }
}
like image 42
Stephen Paul Avatar answered Jul 19 '26 16:07

Stephen Paul