I am trying to create a function that accepts either keys of an object, or an object referencing both the key and a given value.
When providing an object, I would like to infer the type of the value from the given key.
Currently I'm almost there, the only issue is with the last line: I can provide a combination of any key and any accepted type, whereas I would like to be able to provide only the corresponding type.
Here is the code :
import { HttpContextToken } from '@angular/common/http';
const tokens = {
omitBaseUrl: new HttpContextToken(() => true),
test: new HttpContextToken(() => ({ some: 'value' })),
} as const;
type Tokens = typeof tokens;
type ContextParameter =
{ [K in keyof Tokens]: HttpContextToken<any> } extends Record<infer KK extends keyof Tokens, HttpContextToken<any>>
? Tokens[KK] extends HttpContextToken<infer X>
? KK | { context: KK; value: X }
: never
: never;
export function withContext(...contexts: ContextParameter[]) {}
withContext('omitBaseUrl');
withContext('test');
withContext({ context: 'omitBaseUrl', value: true });
withContext({ context: 'test', value: { some: '' } });
withContext('xxx');
withContext(12);
withContext({ context: 'omitBaseUrl', value: 12 });
withContext({ context: 'omitBaseUrl', value: { some: '' } });
The Record<K, V>
utility type doesn't track individual key-value type mappings. Instead it associates all keys with the union of values. If you try to infer {a: string, b: number}
to Record<infer K, infer V>
you'll get Record<"a" | "b", string | number>
and thus {a: string | number, b: string | number}
. Similarly if you index into an object type with its full union of keys, you'll get its full union of value types. So {a: string, b: number}["a" | "b"]
is just string | number
. Your ContextParameter
definition does both of these, hopelessly mixing all property value types together.
Instead, you should consider using a distributive object type as coined in microsoft/TypeScript#47109. Instead of inferring, just map over every property key K
of Tokens
and do your analysis there. Then when you're done, index into that mapped type to get the union of analyzed types. This way, K
will always be a single key and your computed type won't mix them:
type ContextParameter =
{ [K in keyof Tokens]: Tokens[K] extends HttpContextToken<infer X>
? K | { context: K; value: X }
: never }[keyof Tokens]
/* type ContextParameter = "omitBaseUrl" | "test" | {
context: "omitBaseUrl";
value: boolean;
} | {
context: "test";
value: {
some: string;
};
} */
That looks like what you want; the context
/value
pairs are in separate union members as desired.