Specify return type in TypeScript arrow function

Typescript

Typescript Problem Overview


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?

Typescript Solutions


Solution 1 - Typescript

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.

Solution 2 - Typescript

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}; 
};

Solution 3 - Typescript

There are 2 ways of achieving this with proper typing and minimal code:

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

// Because the this keyword works different in arrow functions these 
// 2 implementations are different in some cases:

// arrow function form/ function expression
const addTodo1 = (text: string): AddTodoAction => ({
    type: "ADD_TODO",
    text: text
})

// function declaration form
function addTodo2 (text: string): AddTodoAction {
    return ({
        type: "ADD_TODO",
        text: text
    })
}

Now the TS compiler can check the returned types. For example:

const todo = addTodo1('hi');

// Following gives TS compile time error
// addTodo1 returns AddTodoAction which does not have id on the type

const id = todo.id // Property 'id' does not exist on type 'AddTodoAction'.

Solution 4 - Typescript

https://www.typescriptlang.org/docs/handbook/functions.html

Typing the function

function add(x: number, y: number): number {
  return x + y;
}
 
let myAdd = function (x: number, y: number): number {
  return x + y;
};

Writing the function type with return type:

let myAdd: (x: number, y: number) => number = function (
  x: number,
  y: number
): number {
  return x + y;
};

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBrian HanechakView Question on Stackoverflow
Solution 1 - TypescripthelmbertView Answer on Stackoverflow
Solution 2 - TypescriptaziumView Answer on Stackoverflow
Solution 3 - TypescriptWillem van der VeenView Answer on Stackoverflow
Solution 4 - TypescriptRafiqView Answer on Stackoverflow