Typescript: Type alias results in strange type when returning object with same keys but different values

Viewed 206

I implemented a function (let's call it valuesOfThings) that takes an object with arbitrary key names and values of a specific object type Thing (effectively a Map). The function should then return an object with the same keys, but the value for each key should be the value of some key of Thing.

For example I want to convert this:

const things = {
    someThing: { value: 10 },
    otherThing: { value: 'foo'},
}

to this:

const values = {
    someThing: 10,
    otherThing: 'foo',
}

and I want the returned object to be typed correctly.

The JS implementation of the function looks like this:

function valuesOfThings(things) {
    const values = Object.fromEntries(
        Object.entries(things)
            .map(([key, thing]) => [key, thing.value])
    //                                   ^^^^^^^^^^^----- the important part
    )
    return values
}

Based on this question I managed to craft the following return type:

{[key in keyof T]: T[key] extends Thing<infer ValueType> ? ValueType : never}

Let's see it in action:

type Thing<T> = {
    value: T
}
type ThingMap = {
    [key: string]: Thing<unknown>
}

function valuesOfThings_1<T extends ThingMap>(things: T): {[key in keyof T]: T[key] extends Thing<infer ValueType> ? ValueType : never} {
    const values = Object.fromEntries(
        Object.entries(things)
            .map(([key, thing]) => [key, thing.value])
    )
    return values as {[key in keyof T]: T[key] extends Thing<infer ValueType> ? ValueType : never}
}

As you can see, the return type is relatively lengthy and has to be repeated when returning. But at least it results in the correct type and when I hover the returned variable, I see the type I expected:

Correct type

Bu when I try to extract this type like so:

type ThingValuesMap<T> = {
    [key in keyof T]: T[key] extends Thing<infer ValueType> ? ValueType : never
}

function valuesOfThings<T extends ThingMap>(things: T): ThingValuesMap<T>  {
    return Object.fromEntries(
        Object.entries(things)
            .map(([key, thing]) => [key, thing.value])
    ) as ThingValuesMap<T>
}

the resulting type is not correctly resolved:

Strange type

However, the intellisense knows the correct types:

Intellisense works

As you can see, the type of otherThing is correctly displayed as string.

I've created a TS playground here. Hover values_1 and values_2 to see the difference.

Why is the type not displayed correctly when extracting the return type? Am I doing something wrong here?

1 Answers

The printing of types in typescript is not something that is well defined and changes frequently based on feedback. In most cases typescript will try to preserve type aliases (as they probably have meaning). If you use an inline type that will get resolved as there is no other alternative.

At the time of writing there is a trick to force expansion of a type alias (although to what extent this will always work is not guaranteed). You can use an intersection with the empty object type. This will cause the compiler to expand the type without altering it's structure:


type ThingValuesMap<T> = {} & {
    [key in keyof T]: T[key] extends Thing<infer ValueType> ? ValueType : never
}

Playground Link

Related