Regrettably, TypeScript does not automatically deduce type arguments for generic types such as your FormatEntry
. Type arguments are only inferred when you invoke generic functions in TypeScript. This limitation has been acknowledged as a missing feature and has been requested to be implemented (microsoft/TypeScript#32794). If this feature were to be added, you could potentially write:
// NOT VALID TYPESCRIPT, DO NOT TRY THIS
type FormatEntry<K extends keyof Obj = infer> = {
accessor: K,
formatter: (value: Obj[K]) => string
}
const s: FormatEntry = { accessor: 's', formatter: s => s.toUpperCase() }
// ^? const s: FormatEntry<"s">
This hypothetical code uses the default keyword "infer" in TypeScript's type parameter declaration and demonstrates how automatic inference would work if supported by the language. Until then, you will need to find alternative solutions.
One workaround is to use a generic helper function that mimics the behavior of your desired type:
const formatEntry = <K extends keyof Obj>(f: FormatEntry<K>) => f;
Instead of directly assigning values like const s: FormatEntry = {⋯}
, you can now assign using the helper function like this: const s = formatEntry({⋯})
:
const s = formatEntry({ accessor: 's', formatter: s => s.toUpperCase() });
// ^? const s: FormatEntry<"s">
This approach enables TypeScript to properly infer the type argument K
based on the input to the formatEntry()
function, resulting in the correct type assignment for the variable s
.
In the context of the provided example, you may not necessarily require FormatEntry
to be generic at all. It appears that conceptually, FormatEntry
could be represented as a union of its possible generic versions. Essentially, you might want a type equivalent to
FormatEntry<"i"> | FormatEntry<"b"> | FormatEntry<"s">
:
type FormatEntry = {
accessor: "i";
formatter: (value: number) => string;
} | {
accessor: "b";
formatter: (value: boolean) => string;
} | {
accessor: "s";
formatter: (value: string) => string;
}
This setup creates a discriminated union where accessor
acts as the discriminant property. By leveraging this structure, TypeScript can correctly infer the parameter type of formatter
based on the value of the accessor
property.
To streamline the creation of this union type without manual intervention, we can compute the desired type based on the existing object Obj
:
type FormatEntry = { [K in keyof Obj]: {
accessor: K,
formatter: (value: Obj[K]) => string
} }[keyof Obj];
This technique utilizes a distributive object type pattern, allowing TypeScript to derive the union type FormatEntry
from the original definition of FormatEntry<K>
for each key K
in keyof Obj
, thus eliminating the need for manual type declarations.
Link to TypeScript Playground with Code Example