Here's a solution:
interface ResultMap {
'a': number;
'b': string[];
}
function getValue<K extends keyof ResultMap>(key: K): ResultMap[K];
function getValue(key: keyof ResultMap): ResultMap[keyof ResultMap] {
if (key === 'a') return 5;
if (key === 'b') return ['a', 'b'];
throw new Error();
}
const result1 = getValue('a'); // const result1: number
const result2 = getValue('b'); // const result2: string[]
// Argument of type '"c"' is not assignable to parameter of type '"a" | "b"'.ts(2345)
const result3 = getValue('c'); // const result3: number | string[]
Take note of the function overloading syntax:
function getValue<K extends keyof ResultMap>(key: K): ResultMap[K];
function getValue(key: keyof ResultMap): ResultMap[keyof ResultMap] { // ...
If you only used the second signature, the returned type would always be number | string[]
.
If you only used the first signature, then ResultMap[K]
would be evaluated as number & string[]
, which is contravariant and can lead to errors described in your question.
Alternatively, if you prefer not to use overloaded signatures, you can cast the result inside the function's body:
function getValue<K extends keyof ResultMap>(key: K): ResultMap[K] {
if (key === 'a') return 5 as ResultMap[K];
if (key === 'b') return ['a', 'b'] as ResultMap[K];
throw new Error();
}