When TypeScript encounters difficulty understanding that (T & Record<K, U>)[K]
is equal to or a subtype of U
, I tend to expand T & Record<K, U>
to Record<K, U>
through reassignment. This is because TypeScript does recognize that the simpler Record<K, U>[K]
can be assigned to U
. For example:
function foo<K extends `a${string}`>(k: K) {
const a = {} as { b: 1 } & Record<K, string>
const _a: Record<K, string> = a; // widen a
const r = _a[k];
r.toLowerCase();
}
In this scenario, _a
is essentially just a
, but its type has been expanded from
{b: 1} & Record<K, string>
to
Record<K, string>
. Once you have this,
_a[k]
can then be assigned to
string
.
It's important to note that TypeScript isn't always sound and may not cover all scenarios, so generic type manipulations like these may succeed or fail unexpectedly. The practice of widening T & Record<K, U>
to Record<K, U>
is typically safe, but there are deficiencies in TS's type system that can disrupt equivalences in certain cases. If all else fails and you believe your approach is secure, resorting to a type assertion like const r = a[k] as string
is acceptable.
Click here for Playground link to code