I have following nested object as state.
interface name {
firstName: string;
lastName: string;
}
type NameType = name;
interface employer {
name: string;
state: string;
}
type EmployerType = employer;
interface person {
name: NameType;
age: number;
employer: EmployerType;
}
type PersonType = person;
const defaultPerson: PersonType = {
name: {
firstName: "The",
lastName: "Rock"
},
age: 25,
employer: {
name: "Noone",
state: "Nowhere"
}
};
To update the nested object in state when I use [...] spread operator at second level in case of useState hook, it works just as expected See this working code.
export default function App() {
const [person, setPerson] = useState<PersonType>(defaultPerson);
function handleInputChange(input: string) {
setPerson({
...person,
name: {
...person.name,
firstName: input
}
});
}
return (
<div className="App">
<input onChange={(e) => handleInputChange(e.target.value)} />
<h2>{JSON.stringify(person, null, 4)}</h2>
</div>
);
}
But if I do same thing with a reducer and useReducer hook, Typescript is not liking it and gives error that I am not able to understand. The type error can be seen in codesandbox. See this broken code.
interface action {
type: string;
fieldName: string;
value: string | number;
}
type ActionType = action;
function reducer(state: PersonType, action: ActionType) {
switch (action.type) {
case "firstName": {
return {
...state,
name: {
...state.name,
firstName: action.value
}
};
}
}
return state;
}
export default function App() {
const [person, dispatch] = useReducer(reducer, defaultPerson);
return (
<div className="App">
<input
onChange={(e) =>
dispatch({
type: "test",
fieldName: "firstName",
value: e.target.value
})
}
/>
<h2>{JSON.stringify(person, null, 4)}</h2>
</div>
);
}
Though I am able to cope the state to an entirely new object and update that new object instead, but that feels like a hack and not the right way.
const newState = {...state}
const newName = {...newState.name}
newName.firstName = action.value
newState.name = newName
return newState
CodePudding user response:
interface anti-pattern
You are using interface
and type
unconventionally. Don't create an interface for every type -
type Name = {
firstName: string;
lastName: string;
};
type Employer = {
name: string;
state: string;
};
type Person = {
name: Name;
age: number;
employer: Employer;
};
We should specify the Person
return type for reducer
even though TS can figure it out. However we have another problem, Action
type doesn't match -
function reducer(state: Person, action: Action) {
switch (action.type) {
case "firstName":
return {
...state,
name: {
...state.name,
firstName: action.value // <- string | number
}
};
default:
return state;
}
}
We could just make action.value
a string
, but that means our reducer will only be able to set string values ...
type Action = {
type: string;
fieldName: string;
value: string; // ?
};
kinds of actions
Having an Action that can only set strings would not be very useful. I think a good for supporting a wide-variety of well-type actions would be a tagged union -
enum ActionKind {
SetName,
SetAge,
SetEmployer
}
type Action =
| { kind: ActionKind.SetName; name: Name }
| { kind: ActionKind.SetAge; age: number }
| { kind: ActionKind.SetEmployer; employer: Employer };
| ...
| ...
| ...
function reducer(state: Person, action: Action): Person {
switch (action.kind) {
case ActionKind.SetName:
return { ...state, name: action.name };
case ActionKind.SetAge:
return { ...state, age: action.age };
case ActionKind.SetEmployer:
return { ...state, employer: action.employer };
default:
return state;
}
}
Don't forget to fix dispatch
with our new Action
type -
<input
onChange={(e) =>
// update user's first name
dispatch({
kind: ActionKind.SetName,
name: { firstName: e.target.value, lastName: person.name.lastName }
})
}
/>
CodePudding user response:
Very odd error message indeed. But if you add a return type to your reducer function reducer(state: PersonType, action: ActionType): PersonType {
you get another error message that is more sensible: action.value
might be a number and that is not assignable to firstName
.
So you should probably create more specified ActionType
s.
interface numberAction {
type: string;
fieldName: "firstName" /* | possibly other */;
value: string;
}
interface stringAction {
type: string;
fieldName: "age" /* | possibly other */;
value: number;
}
type ActionType = numberAction | stringAction