Home > Software design >  Getting string literal from a generic object
Getting string literal from a generic object

Time:11-21

This function gives me a type with an index of [x:string] instead of myKey:

const myObj = {
  key: "myKey",
  otherStuff: {stuff:3}
}

type SomeObj<T extends string> = {key: T} & Record<string, any>

function makeObject<TKey extends string, TEntry extends SomeObj<TKey>>(entry: TEntry){ return {[entry.key]: entry} 
}

const result = makeObject(myObj)

/// type of result is const result: {
///    [x: string]: {
///        key: string;
///        otherStuff: {
///            stuff: number;
///        };
///    };
/// }

Same thing for the type of key in the result object. Why is it losing the type information and how can I get it back?

(I realize I can force it to read the literal by declaring key: "myKey" as const, but that's not what I want. I want to infer it in the function, if possible.)

CodePudding user response:

IF you want to infer exact return type, you should overload your function or use type assertion. I prefer the first way. Consider this example:

function makeObject<TKey extends string, TEntry extends SomeObj<TKey>>(entry: TEntry): Record<TEntry['key'], TEntry>
function makeObject<TKey extends string, TEntry extends SomeObj<TKey>>(entry: TEntry){
    return {
        [entry.key]: entry
    }
}

const result = makeObject(myObj)

However, it is still does not work. SInce, myObj is mutable object, TS infers key as a string.

Here you have also two options. You either make your object immutable using as const or use literal object as an argument. First approach:


const myObj = {
    key: "myKey",
    otherStuff: { stuff: 3 }
} as const

Second approach:


const myObj = {
    key: "myKey",
    otherStuff: { stuff: 3 }
}

type SomeObj<T extends string> = { key: T } & Record<string, any>

function makeObject<TKey extends string, TEntry extends SomeObj<TKey>>(entry: TEntry): Record<TEntry['key'], TEntry>
function makeObject<TKey extends string, TEntry extends SomeObj<TKey>>(entry: TEntry) {
    return {
        [entry.key]: entry
    }
}

const result = makeObject({
    key: "myKey",
    otherStuff: { stuff: 3 }
})
result.myKey // myKey

Playground I have noticed that people don't like using as const assertion.

TypeScript always infers computed property as a string and not as an exact key. This is by the default.


Consider this exmaple with error

function makeObject<TKey extends string, TEntry extends SomeObj<TKey>>(entry: TEntry): Record<TEntry['key'], TEntry> {
    // error Type '{ [x: string]: TEntry; }' is not assignable to type 'Record<TEntry["key"], TEntry>'.(2322)
    return {
        [entry.key]: entry
    } 
}

Return value {[entry.key]: entry } has indexed type as {[prop:string]:...}. It means that prop might be any string whereas according to return type key can be only TEntry['key']. Hence, {[prop:string]:...} might be considered as a supertype of Record<TEntry['key']> and supertype is not assignable to subtype.

I have used overalodaing, because it is bivariant. It means that it compiles if it assignble to function signature or function signature assignable to overloading type. In our case, {[prop:string]:...} is not assignable to Record<TEntry['key']>, but Record<TEntry['key']> is assignable to {[prop:string]:...}.

Overloading are not so strict, that's why it works. Usually if return type contains generic from input, TS will complain. In order to fis sucj cases you usualy need to overload your function.

  • Related