Reputation: 228
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
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
andkeyof 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 typeBase
with just a few properties and a family of types derived from that. CallingObject.keys
with aBase
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 returnnever[]
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