How do I declare object value type without declaring key type?

If you use a type annotation on a variable like const x: T, or a type assertion on an expression like x as T, then you’re telling the compiler to treat the variable or value as that type. This essentially throws away information about any more specific type that the compiler may have inferred*. The type of x will be widened to T:

const badMapper1: Record<string, Type1> = { foo1: bar1, foo2: bar2 };
const badMapper2 = { foo3: bar3, foo4: bar4 } as Record<string, Type2>;    

export type BadMapperKeys = keyof typeof badMapper1 | keyof typeof badMapper2;
// type BadMapperKeys = string

Instead, you’re looking for something like “the satisfies operator” as requested in microsoft/TypeScript#7481. The idea is that an expression like x satisfies T would verify that x is assignable to type T without widening it to T. If such an operator existed, you could say something like

// INVALID TYPESCRIPT, DON'T TRY IT, IT WON'T WORK:
const mapper1 = { foo1: bar1, foo2: bar2 } satisfies { [key: string]: Type1 }
const mapper2 = { foo3: bar3, foo4: bar4 } satisfies { [key: string]: Type2 }

export type MapperKeys = keyof typeof mapper1 | keyof typeof mapper2;
// type MapperKeys = "foo1" | "foo2" | "foo3" | "foo4"

and be done. Unfortunately there is no direct support for such an operator.


Luckily you can write helper functions to behave similarly. The general form is something like this:

const satisfies = <T,>() => <U extends T>(u: U) => u;

And then instead of x satisfies T you write (the more cumbersome) satisfies<T>()(x). This works because satisfies<T>() produces an identity function of the form <U extends T>(u: U)=>u where the type of the input U is constrained to T, and the return type is the narrower type U and not the wider type T.

Let’s try it:

const mapper1 = satisfies<Record<string, Type1>>()({ foo1: bar1, foo2: bar2 });
const mapper2 = satisfies<Record<string, Type2>>()({ foo3: bar3, foo4: bar4 });
export type MapperKeys = keyof typeof mapper1 | keyof typeof mapper2;
// type MapperKeys = "foo1" | "foo2" | "foo3" | "foo4"

Looks good!


In your case you specifically asked to specify the object value type but not the keys. If you want you can adapt the satisfies function so that you specify the property value type T and let the compiler infer just the keys. Something like this:

const satisfiesRecord = <T,>() => <K extends PropertyKey>(rec: Record<K, T>) => rec;

You can see that it behaves similarly:

const mapper1 = satisfiesRecord<Type1>()({ foo1: bar1, foo2: bar2, });
const mapper2 = satisfiesRecord<Type2>()({ foo3: bar3, foo4: bar4, });
export type MapperKeys = keyof typeof mapper1 | keyof typeof mapper2;
// type MapperKeys = "foo1" | "foo2" | "foo3" | "foo4"

Playground link to code


*This is not strictly true when you annotate a variable as a union type; in such cases the compiler will narrow the type of the variable upon assignment. But since Record<string, Type1> is not a union type, this is not applicable to the current situation.

Leave a Comment