Home > Back-end >  Mapped typed within mapped type generic does work, except it doesnt
Mapped typed within mapped type generic does work, except it doesnt

Time:09-02

Here is the ts playground

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.

Playground


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.

Playground

  • Related