Brian Hanechak Brian Hanechak - 1 month ago 10
TypeScript Question

Specify return type in TypeScript arrow function

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 TodoActoin = 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?

Answer

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.

Comments