简体   繁体   中英

Typescript doesn't infer correct type with conditionals

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)
}

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: '' })

Playground

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM