Redux-toolkit: How to describe the shape of the state in createSlice

Created on 25 Jan 2019  路  3Comments  路  Source: reduxjs/redux-toolkit

While doing some testing I found that i have to explictly cast my initial state to my desired type if I want to have type safety and auto completion for a more complex state. If I provide the shape as a generic argument I loose all type safety.
Example:

type Todo = {
    title: string,
    isDone: boolean
}

export const todos = createSlice<Todo[]>({ 
    slice: 'todos', 
    initialState: [],
    reducers: {
        addTodo: (state, action: PayloadAction<Todo>) => [...state, action.payload]
    }
});

todos.actions. // no more autocomplete | typesafety
todos.actions.addTodo({ oops: 4}) // I can provide whatever arguments I want here

The second approach works just fine and isn't a big issue. Is this intended behaviour?
```typescript
export const todos = createSlice({
slice: 'todos',
initialState: [] as Todo[],
reducers: {
addTodo: (state, action: PayloadAction) => [...state, action.payload]
}
});

todos.actions.addTodo // I get autocomplete
todos.actions.addTodo({isDone: false, title:"asd"}) // TS forces me to provide the right arguments here
``

Most helpful comment

This is an unfortunate consequence of how type argument inference works in TypeScript: as soon as you specify at least one of them, inference is not attempted for the others; instead, the default types are used instead.

There is ongoing work on a partial type inference feature that will let you pass a _ placeholder for types you want to have inferred, while still letting you specify others concretely. According to the roadmap, this feature is scheduled for TypeScript 3.4 (March 2019). Once that's supported, you could write:

export const todos = createSlice<Todo[], _, _>({ 
  slice: 'todos', 
  initialState: [],
  reducers: {
    addTodo: (state, action: PayloadAction<Todo>) => [...state, action.payload]
  }
});

todos.actions. // autocomplete!
todos.actions.addTodo({ oops: 4}) // type error!

In the meantime, you can omit the type parameters, but use as to cast the initial state to the desired state type. This will give the TypeScript the information it needs to infer correctly.

export const todos = createSlice({ 
  slice: 'todos', 
  initialState: [] as Todo[], // <-- let TypeScript knows which state type you intended
  reducers: {
    addTodo: (state, action: PayloadAction<Todo>) => [...state, action.payload]
  }
});

All 3 comments

This is an unfortunate consequence of how type argument inference works in TypeScript: as soon as you specify at least one of them, inference is not attempted for the others; instead, the default types are used instead.

There is ongoing work on a partial type inference feature that will let you pass a _ placeholder for types you want to have inferred, while still letting you specify others concretely. According to the roadmap, this feature is scheduled for TypeScript 3.4 (March 2019). Once that's supported, you could write:

export const todos = createSlice<Todo[], _, _>({ 
  slice: 'todos', 
  initialState: [],
  reducers: {
    addTodo: (state, action: PayloadAction<Todo>) => [...state, action.payload]
  }
});

todos.actions. // autocomplete!
todos.actions.addTodo({ oops: 4}) // type error!

In the meantime, you can omit the type parameters, but use as to cast the initial state to the desired state type. This will give the TypeScript the information it needs to infer correctly.

export const todos = createSlice({ 
  slice: 'todos', 
  initialState: [] as Todo[], // <-- let TypeScript knows which state type you intended
  reducers: {
    addTodo: (state, action: PayloadAction<Todo>) => [...state, action.payload]
  }
});

Thanks for the clear explanation. I really gotta step up my typescript game.
This approach is fine then.

Couldn't you just define the initial state outside the createSlicer function and add the definition there?

E.g.

const initialToDoState: Todo[] = []

export const todos = createSlice({ 
  slice: 'todos', 
  initialState: initialToDoState,
  reducers: {
    addTodo: (state, action: PayloadAction<Todo>) => [...state, action.payload]
  }
});
Was this page helpful?
0 / 5 - 0 ratings