edited: fixed "test" to "constraint" in interfaces
I am having trouble understanding how generic constraints are working in Typescript. I am working with React/Redux where you can find construction similar to this one:
interface SomeConstraint { constraint: any }
type SomeType<T> = <A extends SomeConstraint>(item: T, action: A) => void;
As you can see, this is a function definition with generic parameters and some constraint. Now, when I want to use it, I can write the following:
interface Test { constraint: string }
const some : SomeType<string> = (s: string, c: Test ) => {}
Which works great, but then, when I want to extend the test interface (let's call it TestWithData):
interface TestWithData { constraint: string, payload: any }
const some : SomeType<string> = (s: string, c: TestWithData ) => {}
I get a compile time error:
error TS2322: Type '(s: string, c: TestWithData) => void' is not assignable to type 'SomeType'. Types of parameters 'c' and 'action' are incompatible. Type 'A' is not assignable to type 'TestWithData'. Type 'SomeConstraint' is not assignable to type 'TestWithData'. Property 'test' is missing in type 'SomeConstraint'.
What I am missing?
Edited
As I said I found this (similar) construct in redux typings. You can find this definition (redux "3.7.2"):
export type Reducer<S> = <A extends Action>(state: S, action: A) => S;
Now when you want to define your reducer you need to provide state and action, I'll skip the state part, so, I can write action to something like this:
interface MyAction { type: "MY_ACTION" }
and creating reducer is easy:
const reducer: Reducer<MyState> = (state: MyState, action: MyAction) => {...}
which will pass compilation, but if I add additional data to MyAction as following:
interface MyAction {
type: "MY_ACTION";
payload: any
}
compilation will fail with the mentioned error. I was suspecting that it would pass. So what is the purpose for this constraint? To tell compiler that we expect exactly the same type (structurally) and nothing more or less? I think that this is a very limited use case, I would expect that the type inference would pick the type, check it's structural compatibility and preserve the type signature. Something like this but without need to specify the type argument:
export type Reducer<S, A extends Action> = (state: S, action: A) => S;
Now, type signature is preserved, but we need to specify the actual parameter type when declaring a variable (same as we do for state):
const reducer: Reducer<MyState, MyAction> = (state: MyState, action: MyAction) => {...}