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.
Answer from helmbert on Stack OverflowVideos
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.
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'.
Edit
Per @Thomas comment, in newer TS compilers, we can simply do:
const foo = <T,>(x: T) => x;
Original Answer
The full example explaining the syntax referenced by Robin... brought it home for me:
Generic functions
Something like the following works fine:
function foo<T>(x: T): T { return x; }
However using an arrow generic function will not:
const foo = <T>(x: T) => x; // ERROR : unclosed `T` tag
Workaround: Use extends on the generic parameter to hint the compiler that it's a generic, e.g.:
const foo = <T extends unknown>(x: T) => x;
If you're in a .tsx file you cannot just write <T>, but this works:
const foo = <T, >(x: T) => x;
As opposed to the extends {} hack, this hack at least preserves the intent.