I want to create a type, which creates an object out of the keys of a given object
, with the values being another object created out of the union type of the given object
.
// It works, but Typescript is unhappy
type makeSortOptions<T extends Record<"albums" | "tracks", string>> = {
[key in keyof T]: { [value in T[key]]: "ascending" | "descending" }
}
type SortOptions = makeSortOptions<
{
albums: "title"
tracks: "title" | "artist"
}
>
// SortOptions is of the expected type
// but why is Typescript unhappy with the utillity type?
const x: SortOptions = {
albums: { title: "descending" },
tracks: {
artist: "descending",
title: "ascending",
},
}
// Does as expected not work
const y: SortOptions = {
albums: { title: "space and time" },
tracks: {
artist: "foo",
bar: "ascending",
},
}
And that is the error I am getting:
Type 'T[key]' is not assignable to type 'string | number | symbol'.
Type 'T[keyof T]' is not assignable to type 'string | number | symbol'.
Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'string | number | symbol'.
Type 'T[string]' is not assignable to type 'string | number | symbol'.ts(2322)
CodePudding user response:
TypeScript is not happy because T[key]
could theoratically be a type which is not string
, number
or symbol
.
Imagine this call:
type SortOptions = makeSortOptions<
{
albums: "title"
tracks: "title" | "artist"
zzz: { a: 123 }
}
>
It is totally valid to call makeSortOptions
with this object, but the zzz
property contains an object which can not be used as a key.
I would put the valid Keys
in an extra type.
type Keys = "albums" | "tracks"
type makeSortOptions<T extends Record<Keys, string>> = {
[key in Keys]: { [value in T[key]]: "ascending" | "descending" }
}
Instead of mapping over keyof T
(which could lead to non-keyable types), we map over Keys
. And through the constraint T extends Record<Keys, string>
, TypeScript knows that every key of Keys
must lead to a string
type.
You could also just intersect it with a string
.
type makeSortOptions<T extends Record<"albums" | "tracks", string>> = {
[key in keyof T]: { [value in T[key] & string]: "ascending" | "descending" }
}
Non-string
types would then evaluate to never
and would not show up in the type.