When using TypeScript with React + Redux, it is a common pattern to use string literals as discriminators on Action types:
interface Action {
type: string
}
interface Increment extends Action {
type: 'INCREMENT',
incBy: number
}
It would be nice if there were a way to refer directly to the type property and have it inlined, so that for example the following code could be written
type Reduction<TState, TAction> = (state:TState, action: TAction) => TState
const reducer = <TState, TAction extends Action>(reduction: Reduction<TState, TAction>) =>
(state: TState, action: Action) =>
isAction<TAction>(action)
? reduction(state, action)
: state
const isAction = <TAction extends Action>(a: Action): a is TAction =>
a.type === TAction.type // cannot find name 'TAction'
// usage:
const incrementReducer = reducer((s: number, a: Increment) => s + a.incBy)
Currently the only way to do this is to duplicate the string literal, like so:
type Reduction<TState, TAction> = (state:TState, action: TAction) => TState
const reducer = <TState, TAction extends Action>(type: string, reduction: Reduction<TState, TAction>) =>
(state: TState, action: Action) =>
isAction<TAction>(type, action)
? reduction(state, action)
: state
const isAction = <TAction extends Action>(type: string, a: Action): a is TAction =>
a.type === type
// usage:
const incrementReducer = reducer('INCREMENT', (s: number, a: Increment) => s + a.incBy)
I feel like using duplicated strings everywhere is unnecessary boilerplate and bound to cause issues with copy/pasting and typos.
I guess this would mean there would need to be a way to declare an interface as having string literal/compile-time constant properties, otherwise it would not be possible to tell at compile time that TAction.type is a string literal.
There is no runtime representation of TAction.type. Types are compile time only. Also, what happens when type is a union?
interface Increment extends Action {
type: 'INCREMENT'|'DECREMENT',
incBy: number
}
You're right, sorry... I was thinking that generic functions were compiled per-implementation-type but obviously they're just compiled to a single JS function! I guess this can't be done then, that's a shame
Actually, I've had a thought...
I think the generic type constraint should not be an interface but something denoting the string literal discriminant. So the syntax could be something like:
const isAction = <TAction extends string .type>(a: Action): a is TAction =>
a.type === TAction.type
At the moment the isAction function is generated like so:
var isAction = function (a) {
return a.type === TAction.type;
};
However, if it were generated like this:
var isAction$type = function(type) {
return function(a) {
return a.type === TAction.type;
}
}
... then this TypeScript call:
isAction<Increment>(myAction)
... could be generated like this:
isAction('INCREMENT')(myAction);
Perhaps that's a lot of work and complexity for little gain, though - I'm no compiler writer...
Would this work?
class Action {
static readonly type: string = '';
}
interface Action {
readonly type: string
}
class Increment {
static readonly type = 'INCREMENT';
}
interface Increment extends Action {
incBy: number
}
class Decrement {
static readonly type = 'DECREMENT';
}
interface Decrement extends Action {
dcrBy: number
}
const isAction = <TAction extends Action>(a: Action, is: TAction): a is TAction =>
a.type === is.type;
@aluanhaddad this just pushes the string instance out to a property on an object: the object must still be passed in as an argument, in which case the string may as well be passed.
In addition, for the redux use case, Actions must be 'pure' objects (eg not classes)
You can use enum values as interface literals.
enum ActionType {
INCREMENT,
DECREMENT
}
interface Increment {
type: ActionType.INCREMENT;
incBy: number;
}
interface Decrement {
type: ActionType.DECREMENT;
decBy: number;
}
type Action = Increment | Decrement;
You still have to pass the parameter, but it's type safe and you get auto-complete help.
It seems like string enums + the solution posted by @icholy is what you're looking for. Thoughts?
Yes, this approach will work. Thanks!
Most helpful comment
There is no runtime representation of
TAction.type. Types are compile time only. Also, what happens whentypeis a union?