The Payloads[S]
type is a specialized form known as an indexed access type, which relies on a yet-to-be-defined generic type parameter S
.
In the current implementation, the compiler struggles to apply control flow analysis to refine the type parameter S
within a switch
/case
statement. Although it can narrow down the value of key
to something specific like "service1"
, it fails to narrow down the type parameter S
. Consequently, it cannot determine that { a: true }
will be compatible with Payloads[S]
in such scenarios. This caution from the compiler leads to errors as it only accepts values assignable to
Payloads[S]</code without considering the actual type of <code>S
, resulting in an intersection of all possible value types (
{a: boolean; b: string; c: number}
). As long as you don't return such a broad value, the compiler will raise complaints.
There have been ongoing discussions and requests for enhancements on GitHub regarding this issue. Check out microsoft/TypeScript#33014 as an example. For now (as of TS4.6), if you need to work with code structured in this manner, the compiler won't assist you in ensuring type safety. Instead, you may need to rely on techniques like type assertions
const createPayloadAssert = <S extends keyof Payloads>(key: S): Payloads[S] => {
switch (key) {
case 'service1': return { a: true } as Payloads[S]
case 'service2': return { b: 'e' } as Payloads[S]
case 'service3': return { c: 3 } as Payloads[S]
default: throw new Error('undefined service')
}
}
or utilize a single-call-signature overload
function createPayloadOverload<S extends keyof Payloads>(key: S): Payloads[S];
function createPayloadOverload(key: keyof Payloads) {
switch (key) {
case 'service1': return { a: true };
case 'service2': return { b: 'e' };
case 'service3': return { c: 3 };
default: throw new Error('undefined service')
}
}
This approach helps relax restrictions enough to prevent errors, but it also leaves room for mistakes where return values are inadvertently interchanged. At present, this is the most effective way to handle situations involving switch
/case
.
If you're open to refactoring your implementation to a format that allows the compiler to validate the code's integrity, consider indexing into an object:
const createPayload = <S extends keyof Payloads>(key: S): Payloads[S] => ({
service1: { a: true },
service2: { b: 'e' },
service3: { c: 3 }
}[key]);
const createPayloadBad = <S extends keyof Payloads>(key: S): Payloads[S] => ({
service1: { a: true },
service2: { a: true }, // <-- error!
service3: { c: 3 }
}[key]);
This strategy leverages indexed access types that were introduced to TypeScript to represent at the type level what occurs when you index into an object with a key at the value level. By utilizing this method, you inform the compiler that the obtained value corresponds to type Payloads[S]
through indexing a Payloads
value with a S
key, as demonstrated above.
Playground link to code