Reputation: 1
I'm using the same type (Options<ST extends SwitchType) for useStrategy
options parameter and for toPayload
options one. What I would expect is that Typescript could infer the correct type for toPayload
options. Instead, it gives me an error message:
Argument of type 'FirstOptions | SecondOptions' is not assignable to parameter of type 'FirstOptions'. Property 'b' is missing in type 'SecondOptions' but required in type 'FirstOptions'.(2345)
Is this a known Typescript limit or am I missing something?
enum SwitchType {
First = 'first',
Second = 'second'
}
export type FirstOptions = {
a: string
b: number
}
type SecondOptions = {
a: string
}
export type Options<ST extends SwitchType> = ST extends SwitchType.Second ? SecondOptions : ST extends SwitchType.First ? FirstOptions : never
type Strategy<ST> = ST extends SwitchType.Second ? SecondStrategy : ST extends SwitchType.First ? FirstStrategy : never
type ToPayloadFunction<ST extends SwitchType> = (
options: Options<ST>
) => any
type FirstStrategy = {
toPayload: ToPayloadFunction<SwitchType.First>
}
type SecondStrategy = {
toPayload: ToPayloadFunction<SwitchType.Second>
}
const getStrategy = <ST extends SwitchType>(type: ST): Strategy<ST> => {
const firstStrategy: FirstStrategy = {
toPayload: (options) => {
console.log(options)
}
}
const secondStrategy: SecondStrategy = {
toPayload: (options) => {
console.log(options)
}
}
if(type === SwitchType.Second) return secondStrategy as any
if(type === SwitchType.First) return firstStrategy as any
throw new Error('error')
}
const useStrategy = <ST extends SwitchType>(type: ST, options: Options<ST>): void => {
const { toPayload } = getStrategy(type)
// Argument of type 'FirstOptions | SecondOptions' is not assignable to parameter of type //'FirstOptions'.
// Property 'b' is missing in type 'SecondOptions' but required in type 'FirstOptions'
toPayload(options)
}
Upvotes: 0
Views: 183
Reputation: 33041
Consider this example:
enum SwitchType {
First = 'first',
Second = 'second'
}
export type FirstOptions = {
a: string
b: number
}
type SecondOptions = {
a: string
}
export type Strategy = {
[SwitchType.Second]: SecondOptions,
[SwitchType.First]: FirstOptions
}
type StrategyHandlers = {
[S in keyof Strategy]: {
toPayload: (options: Strategy[S]) => void
}
}
const firstStrategy: StrategyHandlers[SwitchType.First] = {
toPayload: (options) => {
console.log(options)
}
}
const secondStrategy: StrategyHandlers[SwitchType.Second] = {
toPayload: (options) => {
console.log(options)
}
}
const STRATEGY: StrategyHandlers = {
[SwitchType.First]: firstStrategy,
[SwitchType.Second]: secondStrategy
}
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
function getStrategy<ST extends SwitchType>(type: ST): UnionToIntersection<Values<StrategyHandlers>>
function getStrategy<ST extends SwitchType>(type: ST) {
return STRATEGY[type]
}
type Values<T> = T[keyof T]
type Params = Values<{
[T in SwitchType]: [T, Strategy[T]]
}>
const useStrategy = ([type, options]: Params): void => {
const { toPayload } = getStrategy(type)
toPayload(options)
}
toPayload
- has a type of union of functions which is usually is not wat you expect, this is why you are getting an error. See my article for more explanation.
Also, I have removed conditional types, because in this case it is perfectly fine to use strategy pattern without conditionals, just Map data stracture.
Now, toPayload
is overloaded function which expects either First or Second type of argument.
I can't say that this is 100% safe, because it allows you to do this:
const useStrategy = ([type, options]: Params): void => {
const { toPayload } = getStrategy(type)
toPayload({ a: 's' })
}
Also, see this answer.
The most safer solution would be this:
enum SwitchType {
First = 'first',
Second = 'second'
}
export type FirstOptions = {
a: string
b: number
}
type SecondOptions = {
a: string
}
export type Strategy = {
[SwitchType.Second]: SecondOptions,
[SwitchType.First]: FirstOptions
}
type StrategyHandlers = {
[S in keyof Strategy]: {
toPayload: (options: Strategy[S]) => void
}
}
const firstStrategy: StrategyHandlers[SwitchType.First] = {
toPayload: (options) => {
console.log(options)
}
}
const secondStrategy: StrategyHandlers[SwitchType.Second] = {
toPayload: (options) => {
console.log(options)
}
}
const STRATEGY: StrategyHandlers = {
[SwitchType.First]: firstStrategy,
[SwitchType.Second]: secondStrategy
}
function getStrategy<ST extends SwitchType>(type: ST) {
return STRATEGY[type].toPayload
}
const first = getStrategy(SwitchType.First)
const second = getStrategy(SwitchType.Second)
first({ a: '', b: 2 })
second({ a: '' })
Upvotes: 1