interface Item {
slug: string;
description: string;
}
interface Params {
id: number;
items: Item[];
}
function test(params: Params) {
const result = {};
for (const item of params.items) {
result[item.slug] = "Hello";
}
return result;
}
const data = {
id: 3,
items: [
{ slug: "one", description: "This is one" },
{ slug: "two", description: "This is two" },
{ slug: "three", description: "This is three" },
]
}
const final = test(data)
OK. I have data and pass it to the test function. The test function returns an object with keys as the value of slugs in the items and a string value. So I want the type of final variable to be like this:
{
one: string;
two: string;
three: string;
}
and not this:
Record<string, string>
I mean it must extract slug values from items and create an object with it. explicitly tell the type. How can I define the return type of test function to achieve this?
thanks.
2
Answers
If your data is "static" you can mark it as constant with
as const
now your type for slug is not string but string literals:Or here a playground link
First, you’ll need to change the way you initialize
data
. Without any context, TypeScript looks at the object literal with a property like{slug: "one"}
and infers the type as{slug: string}
. It doesn’t know that you will later want to know the literal type"one"
. That means you’ve lost the information you care about before you even calltest(data)
.The easiest approach is to use a
const
assertion, which infers literal types for literal values, andreadonly
tuples for array literals:Now we can focus on
test()
and the types it operates on. If you want to keep track of the actual literal types of theslug
properties passed intotest()
, so that the output type oftest
depends on its input type, you’ll need to changetest()
‘s call signature to be generic. That implies thatParams
andItem
also need to be generic. Possibly like this:These types are generic in
K
, the type of theslug
property. I’ve givenK
a [default type argument](Note that https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#generic-parameter-defaults) of juststring
, so that you can keep referring to theItem
andParams
types without a type argument and it will be the same as before.Oh, and I made
items
areadonly
array becauseconst
assertions producereadonly
arrays, and I didn’t want to have to make everything more complicated by trying to work around that. Generally speaking if you want to accept an array and you don’t intend to mutate it, you can addreadonly
to the type and it will work just as well. All arrays in TypeScript are assignable to theirreadonly
versions but not vice versa (that is, everystring[]
is considered areadonly string[]
, but not everyreadonly string[]
is considered astring[]
). The namereadonly
is actually confusing that way; really,readonly string[]
means "a readable array ofstring
s, which may or may not be writable" andstring[]
means "a readable and writable array ofstring
s".So, finally, we can make
test
generic and accept aParams<K>
:And I’ve asserted that
result
is of type{[P in K]: string}
, a mapped type equivalent toRecord<K, string>
using theRecord
utility type. It means "an object type whose keys are of typeK
and whose properties are of typestring
". It needs an assertion because{}
is clearly not of that type (it has no keys at all), so we have to lie to the compiler. But the assumption is that yourfor
loop makes changes the assertion from a lie to the truth before the function returns. And now the return type oftest
depends onK
in the way you want:Here you can see that
test(data)
infersK
as"one" | "two" | "three"
, and so the return type is{one: string; two: string; three: string}
as desired.Playground link to code