'Typescript generic object in array only match the specified Records

Demo: https://tsplay.dev/Nnavaw

So I have an array with the following definition:

Array<{
      id?: string;
      text?: string;
      date?: Date;
    }>

That interfers with the following implementation:

data: Array<Partial<Record<K, string>> & Partial<Record<H, string | number | null>>>

How can I tell Typescript that the Array can also include other properties other than Partial<Record<K, string>> & Partial<Record<H, string | number | null>>?

Because if I'll pass an array with the following defintion it gives me this error:

Type 'Date' is not assignable to type 'string | number | null | undefined'.

Complete function:

ifAlreadyExistsString<K extends PropertyKey, H extends PropertyKey>(
    data: Array<Partial<Record<K, string>> & Partial<Record<H, string | number | null>>>,
    key: K,
    value: string,
    idKey?: H,
    idValue?: string | number | null
  ): boolean {
    return (
      data.filter((item) => {
        // If the value is found in the data array
        if (item[key] && item[key]?.trim().toLowerCase() === value.trim().toLowerCase()) {
          // Then check if the id of the value matches the found entry
          // If the ids are matching, then you are currently editing this exact entry
          // If the ids are NOT matching, then you have found a duplicate.
          if (idKey && item[idKey] && idValue) {
            return !(item[idKey] === idValue);
          } else {
            // If no idKey is provided, then we have found a duplicate.
            return true;
          }
        }

        return false;
      }).length !== 0
    );
  }


Solution 1:[1]

If you don't pass in an idKey parameter, then the compiler can't use it to infer H. What you'd like to happen here is that H should fall back to never, because Record<never, string | number | null> is just {}, and you don't want to constrain the element type of data. Unfortunately what actually happens is that the compiler uses the data array type to infer H. It guesses that H should be keyof (typeof data)[number], and then complains about it. Oh well.

You need to step in and prevent H from being inferred as anything but possibly never when idKey is not passed.


One way to do this to overload the method so that there is one call signature for idKey being present and another for it being absent. That looks like:

// call signature without idKey
ifAlreadyExistsString<K extends PropertyKey>(
  data: Array<Partial<Record<K, string>>>,
  key: K,
  value: string,
): boolean;

// call signature with idKey
ifAlreadyExistsString<K extends PropertyKey, H extends PropertyKey>(
  data: Array<Partial<Record<K, string>> & Partial<Record<H, string | number | null>>>,
  key: K,
  value: string,
  idKey?: H,
  idValue?: string | number | null
): boolean;

// implementation
ifAlreadyExistsString<K extends PropertyKey, H extends PropertyKey>(
  data: Array<Partial<Record<K, string>> & Partial<Record<H, string | number | null>>>,
  key: K,
  value: string,
  idKey?: H,
  idValue?: string | number | null
): boolean { /* snip impl */ }

You'll find that this now works:

console.log(this.ifAlreadyExistsString(data, 'text', 'no text')); // okay
/* AppComponent.ifAlreadyExistsString<"text">(
    data: Partial<Record<"text", string>>[], key: "text", value: string
  ): boolean (+1 overload) */

Another thing you could do is try to tell the compiler not to infer H from data, and to default to never. You want to say that the H in the type of data is a non-inferential type parameter usage. There's a feature request at microsoft/TypeScript#14829 for a syntax to do this. The idea is that NoInfer<H> would evaluate to exactly H, but would not be usable for inference.

The feature request is still open. However, there are some ways to emulate NoInfer<T> with current TypeScript features, which work in at least some circumstances. One way is:

type NoInfer<T> = [T][T extends any ? 0 : never];

You can see that no matter what T is, NoInfer<T> will eventually evaluate to T. But the compiler defers evaluation of T extends any ? 0 : never for generic T, and this will block inference:

ifAlreadyExistsString<K extends PropertyKey, H extends PropertyKey = never>(
  data: Array<Partial<Record<K, string>> & Partial<Record<NoInfer<H>, string | number | null>>>,
  key: K,
  value: string,
  idKey?: H,
  idValue?: string | number | null
): boolean { /* snip impl */ }

And now this works again:

console.log(this.ifAlreadyExistsString(data, 'text', 'no text'));
/* AppComponent.ifAlreadyExistsString<"text", never>(
     data: (Partial<Record<"text", string>> & Partial<Record<never, string | number | null>>)[], 
     key: "text", value: string, idKey?: undefined, 
     idValue?: string | number | null | undefined): boolean */

Playground link to code

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 jcalz