Reputation: 1789
recently get into typescript world and saw sth confusing which is related to generic. I saw some code below. I know the <> after "Dispatch" is to describe the type of "Dispatch" but why there is the ?
Does it act as the extra type information for describing T which is used in "action"? Thank you very much
Upvotes: 0
Views: 96
Reputation: 327964
Let's examine this interface:
interface Dispatch<A extends Action = AnyAction> {
<T extends A>(action: T): T;
}
First of all, the interface Dispatch
is generic in A
, so you can specify A
to get a concrete type from it. The type A
specified is constrained to be something assignable to Action
, and if you don't specify the type it will default to AnyAction
:
let actionDispatcher: Dispatch<Action>; //okay
let anyActionDispatcher: Dispatch; // Dispatch<AnyAction>
let badDispatcher: Dispatch<string>; // error!
// not an Action ---------> ~~~~~~
interface FooAction extends Action {
foo: string;
}
let fooDispatcher: Dispatch<FooAction>; // okay
So once you specify A
, you get a concrete type by replacing all instances of A
with the type you specify. Let's look at Dispatch<FooAction>
. You get this:
// same type as Dispatch<FooAction>
interface DispatchFooAction {
<T extends FooAction>(action: T): T;
}
So, now the question is: what is a DispatchFooAction
?
Well, the interface has a single call signature, so it can be used as a function. Let's look at the signature: <T extends FooAction>(action: T): T
.
That is a generic function signature whose type parameter T
must be assignable to FooAction
(or whatever A
is specified in Dispatch<A>
); it takes a parameter of type T
and returns a value of the same type.
Let's see if we can implement and use a Dispatch<FooAction>
:
// good implementation
fooDispatcher = <T extends FooAction>(a: T) => a; // okay
const x = fooDispatcher({ type: "foo", foo: "" }); // okay,
// T inferred as { type: string; foo: string; }, x is of that type
const y = fooDispatcher({ type: "foo", foo: "", baz: 1 }); // okay,
// T inferred as { type: string; foo: string; baz: number; }, y is of that type
const z = fooDispatcher({ type: "foo" }); // error!
// missing foo prop --> ~~~~~~~~~~~~~~~
interface BarAction extends FooAction {
bar: string;
}
// bad implementation
fooDispatcher = <T extends BarAction>(a: T) => a; // error!
//~~~~~~~~~~~ <-- bar missing in FooAction but required in BarAction
The good implementation works; fooDispatcher
is a valid Dispatch<FooAction>
because it's a function that takes an input of FooAction
or any subtype of it, and returns the same type. You can see that x
and y
call the function correctly, but z
does not, because the input is not a valid FooAction
.
The bad implementation doesn't work; <T extends BarAction>(a: T)=>a
only claims to accept BarAction
inputs, but fooDispatcher
is required to accept any FooAction
, and not every FooAction
is a BarAction
.
So, hopefully that makes some sense and that it helps you. Good luck!
Upvotes: 1
Reputation: 347
export interface Dispatch<A extends Action = AnyAction> { <T extends A>(action: T): T }
This declaration is a call signature. An equivalent declaration would be:
export type Dispatch<A extends Action = AnyAction> =
<T extends A>(action: T) => T
This, again, can be seen in the TypeScript docs: Generic Types.
Therefore, this means:
any function typed
Dispatch<A>
(where A is a free type variable of which concrete type shall extendAction
and defaults toAnyAction
) receives any value of which type (T
) extendsA
and returns a value of the same type (possibly itself).
The important distinction between the type parameter in the type declaration (here A
) and the function type declaration (here T
) is that the former lets you specify which specific subtype the function implements or the caller should expect it to be, while the latter means the function should accept whatever type that satisfies the constraint and the caller may assume it could accept any such type.
Upvotes: 1