Home > Mobile >  Typescript object literal narrowing
Typescript object literal narrowing

Time:02-17

I've been googling for close to two hours so now I turn to Stack Overflow for help. I am writing a function which accepts an object parameter. I can't figure out how to force TS to narrow to the type when an object literal is passed. My simple working example looks like the code below but it feels unnecessarily cumbersom:

type ValueOf<T> = T[keyof T];

const getRandomValue = <T, S=Record<string, T>>(obj:S):T => {
    const values = Object.values(obj);
    const randomIndex = Math.floor(Math.random() * values.length);

    return values[randomIndex];
}

const obj = {
    a: "hi",
    b: "hello",
} as const;

//this works as expected but it's cumbersome to define the literal separately and have to say "as const" to avoid the type becoming { string: string }
const value = getRandomValue<ValueOf<typeof obj>>(obj);

//prove that it works
type ValueType = typeof value;
const x:ValueType = "hello"; //allowed
const y:ValueType = "bonjour"; //shows a type error

//I was hoping I could define the function in a way where I could call it like this and still
//get the same strict typing as above: getRandomValue({ a: "hi", b: "hello" });

playground link

Edit: removed the non-working version of code.

CodePudding user response:

What happens with your ValueOf<T> when you use T = Record<string, string> is that TS (correctly) determines that Record<string, string>[string] = string.

When you use getRandomValue<T>(obj: T): ValueOf<T> with an object literal, say with { foo: "bar" }, T isn't Record<string, string> but a Record<"foo", "bar">.

Thus you'll need to use generics for the record's key and value types:

const getRandomValueFromObj = <Key, Value>(obj: Record<Key, Value>): Value => {}

This isn't enough, however, for a few different reasons. First, TS will still determine Key = string and Value = string for e.g. { foo: "bar" }, but you want string constants. You could write { foo: "bar" as const } to promise TS that "bar" is constant, but it's easier to use a generic constraint in the function definition:

const getRandomValueFromObj =
    <Key, Value extends string>(obj: Record<Key, Value>): Value => {}

Now typescript will determine Value = "bar" for { foo: "bar" }.

Next up, you'll have trouble with Key and Object.keys, whose signature is .keys(o: {}): string[]. That is, even though o is Record<Key, Value>, Object.keys will always return string[], and that's intentional. You'll have to use a type assertion:

const getRandomValueFromObj =
    <Key, Value extends string>(obj: Record<Key, Value>): Value => {
        const keys = Object.keys(obj) as Key[];
    }

But TS still isn't happy, since Key and string might not overlap. After all, Key could be anything! You'll have to constrain Key, too:

const getRandomValueFromObj =
    <K extends string, V extends string>(obj: Record<K, V>): V => {
        const keys = Object.keys(obj) as Key[];
    }

Here's a working playground link.

  • Related