I followed this link for usage of TypeScript with redux toolkit but still couldn't figure out the issue when it comes to data type.
interface IDashboard {
findGoals: { status: string; data: Goal[]; error: string | null; };
findSchedule: { status: string; data: []; error: string | null; };
}
const initialState: IDashboard = {
findGoals: { data: [], status: ApplicationConstants.IDLE, error: null },
findSchedule: { data: [], status: ApplicationConstants.IDLE, error: null },
};
const dashboard = createSlice({
name: "dashboard",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(findGoals.pending, state => {
state.findGoals = {data: [], status: ApplicationConstants.LOADING, error: null};
})
.addCase(findGoals.fulfilled, (state, action: PayloadAction<Goal[]>) => {
state.findGoals = {data: action.payload, status: ApplicationConstants.SUCCEEDED, error: null};
})
.addCase(findGoals.rejected, (state, action) => {
state.findGoals = {data: [], status: ApplicationConstants.FAILED, error: action.error};
});
builder
.addCase(findSchedule.pending, state => {
state.findSchedule = {data: [], status: ApplicationConstants.LOADING, error: null};
})
.addCase(findSchedule.fulfilled, (state, action) => {
state.findSchedule = {data: action.payload, status: ApplicationConstants.SUCCEEDED, error: null};
})
.addCase(findSchedule.rejected, (state, action) => {
state.findSchedule = {data: [], status: ApplicationConstants.FAILED, error: action.error};
});
}
});
ApplicationConstants
export class ApplicationConstants {
static IDLE = "idle";
static LOADING = "loading";
static SUCCEEDED = "succeeded";
static FAILED = "failed";
}
methods
export const findGoals = createAsyncThunk("dashboard/findGoals",
async (client: ApolloClient<NormalizedCacheObject>) => {
return new Promise((resolve, reject) => {
const GET_GOALS = gql`
query Goals {
goals {
id
title
description
percentage
}
}
`;
client.query({ query: GET_GOALS })
.then(response => resolve(response.data.goals))
.catch(error => reject(error));
});
}
);
Goal
export interface Goal {
id: string;
title: string;
description: string;
percentage: string;
}
Store
export const store = configureStore({
reducer: {
dashboard: dashboard,
}
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Attempt
I used the type for action as action: PayloadAction<Goal[]>
without any success for the first addCase
.
Question:
Please advise on what type signature to use with the action for both fulfilled
(action.payload
) and rejected
(action.error
) cases.
Screenshot: See the red lines.
Edit
If I hover over data I see the following TypeScript warning
TS2740: Type RejectWithValue<unknown, unknown> is missing the following properties from type WritableDraft<Goal>[]: length, pop, push, concat, and 35 more.
dashboard. ts(7, 34): The expected type comes from property data which is declared here on type
and If I hover over error I see the following TypeScript warnings
TS2322: Type SerializedError is not assignable to type string
dashboard. ts(7, 48): The expected type comes from property error which is declared here on type
I was unable to reproduce the exact issue/error you describe here, but I think your implementation is not helping and might actually be working against you. The findGoals
action logic can be simplified a bit using standard async/await
instead of wrapping up the logic in an additional Promise chain.
Example:
export const findGoals = createAsyncThunk(
"dashboard/findGoals",
async (client: ApolloClient<NormalizedCacheObject>) => {
const GET_GOALS = gql`
query Goals {
goals {
id
title
description
percentage
}
}
`;
const response = await client.query({ query: GET_GOALS });
return response.data.goals as Goal[];
}
);
The only additional change I needed to add was to cast the action.error
to string
from its default SerializedError
type.
const dashboard = createSlice({
name: "dashboard",
initialState,
extraReducers: (builder) => {
builder
.addCase(findGoals.pending, (state) => {
state.findGoals = {
data: [],
status: ApplicationConstants.LOADING,
error: null,
};
})
.addCase(findGoals.fulfilled, (state, action) => {
state.findGoals = {
data: action.payload,
status: ApplicationConstants.SUCCEEDED,
error: null,
};
})
.addCase(findGoals.rejected, (state, action) => {
state.findGoals = {
data: [],
status: ApplicationConstants.FAILED,
error: action.error as string, // <--
};
});
builder
.addCase(findSchedule.pending, (state) => {
state.findSchedule = {
data: [],
status: ApplicationConstants.LOADING,
error: null,
};
})
.addCase(findSchedule.fulfilled, (state, action) => {
state.findSchedule = {
data: action.payload,
status: ApplicationConstants.SUCCEEDED,
error: null,
};
})
.addCase(findSchedule.rejected, (state, action) => {
state.findSchedule = {
data: [],
status: ApplicationConstants.FAILED,
error: action.error as string, // <--
};
});
},
});
Also, FWIW, RTK utilizes Immer.js under the hood, so it allows you to write mutable state updates, so there's no need to create new findGoals
and findSchedule
object references, RTK takes care of this for you auto-magically.
Example:
const dashboard = createSlice({
name: "dashboard",
initialState,
extraReducers: (builder) => {
builder
.addCase(findGoals.pending, (state) => {
state.findGoals.status = ApplicationConstants.LOADING;
})
.addCase(findGoals.fulfilled, (state, action) => {
state.findGoals.data = action.payload;
state.findGoals.states = ApplicationConstants.SUCCEEDED;
})
.addCase(findGoals.rejected, (state, action) => {
state.findGoals.status = ApplicationConstants.FAILED;
state.findGoals.error = action.error as string;
});
builder
.addCase(findSchedule.pending, (state) => {
state.findSchedule.status = ApplicationConstants.LOADING;
})
.addCase(findSchedule.fulfilled, (state, action) => {
state.findSchedule.data = action.payload;
state.findSchedule.states = ApplicationConstants.SUCCEEDED;
})
.addCase(findSchedule.rejected, (state, action) => {
state.findSchedule.status = ApplicationConstants.FAILED;
state.findSchedule.error = action.error as string;
});
},
});