Specify return type in TypeScript arrow function
TypescriptTypescript 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;
};