1

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
    );
  }
7
  • data: Array<Partial<Record<K, string>> & Partial<Record<H, string | number | Date | null>>> fixes the error. Is that what you wanted? Commented May 13, 2022 at 8:27
  • No, because you modify the data type of idKey/H. The sturcture of the object could be anything but should include the Record definitions Commented May 13, 2022 at 9:07
  • 1
    Inference is failing to do what you want when idKey is not passed. Two options I see; use overloads like this, or use a NoInfer trick to try to prevent data from being used to infer H, like this. Let me know which, if any, of those meets your needs and I will write up an answer explaining. If neither meets your needs, what am I missing? Commented May 13, 2022 at 22:03
  • @jcalz using the NoInfer trick works fine, thank you! Commented May 16, 2022 at 7:22
  • 1
    Autocompletion seems to be out of scope of the question as asked. Is that needed for my answer? If so, please edit the question to clarify your requirements, preferably by showing use cases. If not, then I'm happy to advise in a different post, asssuming that you can't find the answer to your question by searching SO. Commented May 16, 2022 at 15:15

1 Answer 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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.