user1148920
user1148920

Reputation: 228

No overload matches this call. When using Object reduce callback in Typescript

I've been stuck on this one for a few hours. I'm basically trying to create a function which turns a typescript type into a query string. I started with an example I found online but I can't seem to get it working. I'm seeing the error below.

import querystring from 'querystring';


 type AuthQuery = {
  code: string;
  timestamp: string;
  state: string;
  shop: string;
  host?: string;
  hmac?: string;
}

export function stringifyQuery(query: AuthQuery): string {
  const orderedObj = Object.keys(query)
    .sort((val1, val2) => val1.localeCompare(val2))
    .reduce((obj: Record<string, string | undefined>, key: keyof AuthQuery) => {
      obj[key] = query[key];
      return obj;
    }, {});
  return querystring.stringify(orderedObj);
}

And I get the following:

No overload matches this call.
  Overload 1 of 3, '(callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string, initialValue: string): string', gave the following error.
    Argument of type '(obj: Record<string, string | undefined>, key: keyof AuthQuery) => Record<string, string | undefined>' is not assignable to parameter of type '(previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string'.
      Types of parameters 'obj' and 'previousValue' are incompatible.
        Type 'string' is not assignable to type 'Record<string, string | undefined>'.
  Overload 2 of 3, '(callbackfn: (previousValue: Record<string, string | undefined>, currentValue: string, currentIndex: number, array: string[]) => Record<string, string | undefined>, initialValue: Record<...>): Record<...>', gave the following error.
    Argument of type '(obj: Record<string, string | undefined>, key: keyof AuthQuery) => Record<string, string | undefined>' is not assignable to parameter of type '(previousValue: Record<string, string | undefined>, currentValue: string, currentIndex: number, array: string[]) => Record<string, string | undefined>'.
      Types of parameters 'key' and 'currentValue' are incompatible.
        Type 'string' is not assignable to type 'keyof AuthQuery'.(2769)


Upvotes: 0

Views: 1159

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074355

The problem is that reduce doesn't know that the array it's being called on is of keyof AuthQuery values; it thinks they're strings, because that's what Object.keys provides. You can't fix that by putting a type on the key parameter in the reduce call. Unfortunately, you have to use a type assertion, see where I created sortedKeys below (it doesn't have to be its own separate variable, but things were getting unwieldy):

export function stringifyQuery(query: AuthQuery): string {
    const sortedKeys = Object.keys(query)
        .sort((val1, val2) => val1.localeCompare(val2)) as (keyof AuthQuery)[];
    const orderedObj = sortedKeys
        .reduce((obj: Record<string, string | undefined>, key) => {
            obj[key] = query[key];
            return obj;
        }, {});
    return querystring.stringify(orderedObj);
}

I'd be tempted to move the Record<string, string | undefined> to the end as a type assertion on the {} seed value, like this:

export function stringifyQuery(query: AuthQuery): string {
    const sortedKeys = Object.keys(query)
        .sort((val1, val2) => val1.localeCompare(val2)) as (keyof AuthQuery)[];
    const orderedObj = sortedKeys
        .reduce((obj, key) => {
            obj[key] = query[key];
            return obj;
        }, {} as Record<string, string | undefined>);
    return querystring.stringify(orderedObj);
}

...but it works where you have it, so I've left it there.

I wouldn't use reduce for this, though, reduce only complicates this code. Here's a version using a simple loop:

export function stringifyQuery(query: AuthQuery): string {
    const sortedKeys = Object.keys(query)
        .sort((val1, val2) => val1.localeCompare(val2)) as (keyof AuthQuery)[];
    const orderedObj: Record<string, string | undefined> = {};
    for (const key of sortedKeys) {
        orderedObj[key] = query[key];
    }
    return querystring.stringify(orderedObj);
}

You may be wondering why Object.keys isn't defined like this:

function keys<T>(object: T): (keyof T)[];

Doing that was considered and rejected, apparently because of this observation by Anders Hejlsberg (hugely respected language designer, creator of Turbo Pascal, chief architect of Delphi, lead architect of C#, and core developer of TypeScript):

My reservations about Object.keys returning (keyof T)[] still hold. It makes sense only in the domain of type variables (i.e. T and keyof T). Once you move to the instantiated type world it degenerates because an object can (and often does) have more properties at run-time than are statically known at compile time. For example, imagine a type Base with just a few properties and a family of types derived from that. Calling Object.keys with a Base would return a (keyof Base)[] which is almost certainly wrong because an actual instance would be a derived object with more keys. It completely degenerates for type {} which could be any object but would return never[] for its keys.

If you wanted to ignore that sage wisdom — at your peril! — you could provide yourself a wrapper for it:

function staticKeys<T>(object: T) {
    return Object.keys(object) as (keyof T)[];
}

Then getting the sorted keys for your operation above is staticKeys(query).sort(/*...*/) with no type assertion at the call site.


Side note re sorting the Object.keys array: Although objects do have a defined property order now, it's complicated (depends not only on when the properties were added, but on the actual values of the keys) and using it is rarely useful.

Upvotes: 2

Related Questions