To begin, you must adjust how you initialize the data
. When TypeScript examines the object literal with a property like {slug: "one"}
, it infers the type as {slug: string}
, without recognizing your later intent to identify the literal type "one"
. Therefore, you lose the crucial information even before calling test(data)
.
The simplest solution is to employ a const
assertion, which automatically infers literal types for literal values and uses readonly
tuples for array literals:
const data = {
id: 3,
items: [
{ slug: "one", description: "This is one" },
{ slug: "two", description: "This is two" },
{ slug: "three", description: "This is three" },
]
} as const;
Now we can turn our attention to test()
and the types it interacts with. If you desire to maintain the actual literal types of the slug
properties passed into test()
, thereby making the output type of test
dependent on its input type, you need to redefine test()
's call signature as generic. This implies that Params
and Item
should also be generic, possibly designed like so:
interface Item<K extends string = string> {
slug: K;
description: string;
}
interface Params<K extends string = string> {
id: number;
items: readonly Item<K>[];
}
These types use K
as a generic parameter for the slug
property type. I have assigned K
a [default type argument](Note that https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#generic-parameter-defaults) of simply string
, allowing you to refer to the Item
and Params
types without a specific type argument, maintaining consistency.
Additionally, I've converted items
into a readonly
array due to const
assertions producing readonly
arrays, eliminating unnecessary complications. Generally, if an array is not meant to be modified after acceptance, adding readonly
to the type suffices. Although all arrays in TypeScript are assignable to their readonly
equivalents, the reverse is not true, underscoring the nuances of using readonly
in this context.
Finally, by making test
generic and accepting a Params<K>
, we achieve:
function test(params: Params<K>) {
const result = {} as {[P in K]: string} // <-- need assertion
for (const item of params.items) {
result[item.slug] = "Hello";
}
return result;
}
I've explicitly asserted that result
adheres to the type {[P in K]: string}
, a mapped type akin to Record<K, string>
utilizing the Record
utility type. Essentially, this type denotes "an object type containing keys of type K
and values of type string
". An assertion becomes necessary since {}
does not conform to this structure at first, necessitating alignment within the function's logic flow. Consequently, the return type of test
mirrors the dependency on K
:
const final = test(data)
/* const final: {
one: string;
two: string;
three: string;
} */
This demonstrates that test(data)
identifies K
as "one" | "two" | "three"
, hence returning the desired type
{one: string; two: string; three: string}
.
Access the Playground link here