142
votes

I am using React and Redux and have action types specified as interfaces, so that my reducers can take advantage of tagged union types for improved type safety.

So, I have type declarations that look like this:

interface AddTodoAction {
    type: "ADD_TODO",
    text: string
};

interface DeleteTodoAction {
    type: "DELETE_TODO",
    id: number
}

type TodoAction = AddTodoAction | DeleteTodoAction

I'd like to make helper functions that create these actions, and I tend to use arrow functions for this. If I write this:

export const addTodo1 = (text: string) => ({
    type: "ADD_TODO",
    text
});

The compiler can't provide any help in making sure this is a valid AddTodoAction because the return type isn't specified explicitly. I can specify the return type explicitly by doing this:

export const addTodo2: (text: string) => AddTodoAction = (text: string) => ({
    type: "ADD_TODO",
    text
})

But this requires specifying my function arguments twice, so it's verbose and harder to read.

Is there a way I can specify the return type explicitly when using arrow notation?

I've thought of trying this:

export const addTodo3 = (text: string) => <AddTodoAction>({
    type: "ADD_TODO",
    text
})

In this case, the compiler now infers the return type as AddTodoAction but it's doesn't validate that the object I'm returning has all of the appropriate fields.

I could solve this by switching to a different function syntax:

export const addTodo4 = function(text: string): AddTodoAction {
    return {
        type: "ADD_TODO",
        text
    }
}

export function addTodo5(text: string): AddTodoAction {
    return {
        type: "ADD_TODO",
        text
    }
}

Either of these methods will cause the compiler to use the correct return type and enforce that I have set all fields appropriately, but they are also more verbose and they change the way 'this' is handled in a function (which may not be an issue, I suppose.)

Is there any advice about the best way to do this?

2
getTitle = ():string => 'State Lists'Vikas Bansal

2 Answers

181
votes

First, consider the following notation from your original question:

export const addTodo3 = (text: string) => <AddTodoAction>({
    type: "ADD_TODO",
    text
})

Using this notation, you typecast the returned object to the type AddTodoAction. However, the function's declared return type is still undefined (and the compiler will implicitly assume any as return type).

Use the following notation instead:

export const addTodo3 = (text: string): AddTodoAction => ({
    type: "ADD_TODO",
    text: text
})

In this case, omitting a required property will yield the expected compiler error. For example, omitting the text property will generate the following (desired) error:

Type '{ type: "ADD_TODO"; }' is not assignable to type 'TodoAction'.
  Type '{ type: "ADD_TODO"; }' is not assignable to type 'DeleteTodoAction'.
    Types of property 'type' are incompatible.
      Type '"ADD_TODO"' is not assignable to type '"DELETE_TODO"'.

Also see the playground example.

2
votes

I think your best bet is to create an interface for your function which has the right types, then you only need to specify that type, not all the nested types of your interface:

interface AddTodoAction {
    type: "ADD_TODO",
    text: string
};

interface AddTodoActionCreator {
    (text: string): AddTodoAction;
};

export const addTodo: AddTodoActionCreator = (text) => ({
    type: "ADD_TODO",
    text
});

Update: How to do this with types

export interface GeneralAction<T> {
    type: string;
    payload: T;
}

export interface GeneralActionCreator<T> {
    (payload: T): GeneralAction<T>;
}

export const SAVE_EVENT = 'SAVE_EVENT';

export const SaveEvent: GeneralActionCreator<UserEvent> = (payload) => { return {type: SAVE_EVENT, payload}; };