Typing object literals without widening their keys
When you annotate an object literal with a broad type like Record<string, OperationFn>, TypeScript widens the key type to string. That kills the union type you want from keyof typeof. The workaround is a constrained identity function (CIF): a function that accepts a value, constrains its type, and returns it unchanged.
Consider the earlier calculator component. The operations object maps operator strings to functions of the same shape:
type OperationFn = (left: number, right: number) => number
Typing the variable directly widens the keys:
type OperationFn = (left: number, right: number) => number
const operations: Record<string, OperationFn> = {
'+': (left, right) => left + right,
'-': (left, right) => left - right,
'*': (left, right) => left * right,
'/': (left, right) => left / right,
}
type CalculatorProps = {
left: number
operator: keyof typeof operations
right: number
}
Annotating with an explicit Record<Operation, OperationFn> gets verbose and duplicates keys:
type OperationFn = (left: number, right: number) => number
type Operator = '+' | '-' | '/' | '*'
const operations: Record<Operator, OperationFn> = {
'+': (left, right) => left + right,
'-': (left, right) => left - right,
'*': (left, right) => left * right,
'/': (left, right) => left / right,
}
type CalculatorProps = {
left: number
operator: keyof typeof operations
right: number
}
TypeScript gives no way to say "keys stay narrow, values get checked against this type." A CIF works around that. It's an identity function with a generic constraint:
type Value = number
const createNumbers = <ObjectType extends Record<string, Value>>(
obj: ObjectType,
) => obj
const numbers = createNumbers({ one: 1, two: 2, three: 3, four: 4 })
// @ts-expect-error we don't have 'five' yet
numbers['five']
The first type parameter T captures the actual inferred type — including the literal keys. The second parameter TRecord constrains the values to the shape you need. The function returns the object as given, so inference keeps the narrow key union.
Applying that to the component gives both goals: uniform value typing and a finite keyof union:
type OperationFn = (left: number, right: number) => number
const createOperations = <OperationsType extends Record<string, OperationFn>>(
operations: OperationsType,
) => operations
const operations = createOperations({
'+': (left, right) => left + right,
'-': (left, right) => left - right,
'*': (left, right) => left * right,
'/': (left, right) => left / right,
})
type CalculatorProps = {
left: number
operator: keyof typeof operations
right: number
}
// @ts-expect-error we haven't added support
// for the exponentiation operator yet
operations['**'](1, 2)
No function in the object needs an individual annotation, and adding a case to the switch and an entry to operations stays in sync.
Why not build a generic CIF
You might want one helper that works for any value type, but TypeScript can't express that cleanly. The closest attempt fails because of how generic inference handles the constraint:
const constrain = <Given, Inferred extends Given>(item: Inferred) => item
// @ts-expect-error Expected 2 type arguments, but got 1.(2558)
const numbers = constrain<Record<string, number>>({ one: 1 /* etc. */ })
A workable generic version exists, but it's awkward enough that the abstraction isn't worth it:
const constrain =
<Given extends unknown>() =>
<Inferred extends Given>(item: Inferred) =>
item
const numbers = constrain<Record<string, number>>()({ one: 1 /* etc. */ })
const createNumbers = constrain<Record<string, number>>()
const numbers = createNumbers({ one: 1 /* etc. */ })
TypeScript 4.9: the satisfies operator
TypeScript 4.9 added satisfies, which does exactly what the CIF does without the wrapper function. You annotate the object with the constraint while preserving the inferred literal types:
type OperationFn = (left: number, right: number) => number
const operations = {
'+': (left, right) => left + right,
'-': (left, right) => left - right,
'*': (left, right) => left * right,
'/': (left, right) => left / right,
} satisfies Record<string, OperationFn>
This keeps the narrow key union and checks each value against the operation function type. No helper, no extra generic plumbing.



