typescript inference of generics argument of type not assignable

Viewed 80

I am trying to infer the data type in my doAction from the generic type of my GAction

I don't understand why I get the error message Argument of type 'string | number' is not assignable to parameter of type 'never'. Type 'string' is not assignable to type 'never'. Please help me understand what I am doing wrong.

abstract class Action<GData> {
  abstract isValid(data : GData): boolean

  static dostuff<GData>(action: Action<GData>, data : GData): boolean {
    return action.isValid(data);
  }
}
  abstract class State<GAction extends Action<any>> {
    abstract doAction(action: GAction, data: any): State<GAction> 
  }
  
  class NextAction extends Action<string> {
    isValid(data: string): boolean {
      return (data === "222")
    }
  }
  
  class LastAction extends Action<number> {
    isValid(data : number): boolean {
      return (data === 254);
    }
  
  }
  
  type AllStateActions = LastAction | NextAction 

  type ActionData<S> = S extends Action<infer H> ? H : never;
  class OneState extends State<AllStateActions> {
    doAction<GAction extends AllStateActions, GData extends ActionData<GAction>> (action: GAction , data: GData): OneState {
      if(!action.isValid(data)) {
        return this;
      }
      return this; 
    }
  
  }
1 Answers

The issue is in this line:

type AllStateActions = LastAction | NextAction

Union of function never behaves in an expected way. In fact, I'm not aware of a case where you need to use union of functions.

Consider this example:


type Foo = (v: { name: string }) => void

type Bar = (v: { age: number }) => void

type Union = Foo | Bar

declare const union:Union

// union(v: { name: string; } & { age: number; })
union()

Please see docs:

Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred

Let's go back to our original example:

type AllStateActions = LastAction | NextAction

declare const action:AllStateActions

// isValid(data: never): boolean
action.isValid()

isValid expects never, because string & number === never. Intersection of string and number is unrepresentable.

In order to fix it, you should intersection:

type AllStateActions = LastAction & NextAction

Intersection of two function overloads them.

WHole code:

abstract class Action<GData> {
    abstract isValid(data: GData): boolean

    static dostuff<GData>(action: Action<GData>, data: GData): boolean {
        return action.isValid(data);
    }
}
abstract class State<GAction extends Action<any>> {
    abstract doAction(action: GAction, data: any): State<GAction>
}

class NextAction extends Action<string> {
    isValid(data: string): boolean {
        return (data === "222")
    }
}

class LastAction extends Action<number> {
    isValid(data: number): boolean {
        return (data === 254);
    }
}

type AllStateActions = LastAction & NextAction // <-------------------------- IMPORTANT CHANGE

type ActionData<S> = S extends Action<infer H> ? H : never;

class OneState extends State<AllStateActions> {

    doAction<GAction extends AllStateActions, GData extends ActionData<GAction>>(action: GAction, data: GData): OneState {
        if (!action.isValid(data)) {
            return this;
        }
        return this;
    }
}

Playground

Related