I am having some problem using redux with ts
my initial state is {} and later it is populated with keys and values
so when I try use that state in my component, its saying that this property does not exist on type {}
even though I am sure that this component renders only after the state has been populated.
How can I fix this error?
I am trying to use it like this:
const TopSection = () => {
const { username } = useAppSelector((state) => state.auth);
const mediaDetails: MediaDetailState = useAppSelector((state) => state.media);
const { mediaid, mediaType } = mediaDetails;
const routes = [
{ path: "/", title: "Overview" },
{ path: "watch", title: "Watch" },
{ path: "characters", title: "Characters" },
{ path: "staff", title: "Staff" },
{ path: "stats", title: "Stats" },
{ path: "social", title: "Social" },
];
return (
<div className="bg-bgSecondary">
{/* Backdrop image */}
{mediaDetails.backdrop_path && (
<div className="h-[25vh] md:h-[50vh] overflow-hidden">
<img
src={`${tmdbImgEndPoint}${mediaDetails.backdrop_path}`}
alt={mediaType === "movie" ? mediaDetails.title : mediaDetails.name}
className="object-top"
/>
</div>
)}
{/* Poster and overview */}
<div className="grid grid-cols-12 px-12 md:px-56">
{/* Poster and buttons */}
<div className="col-span-12 md:col-span-2 grid grid-cols-3 gap-4 md:block">
<img
src={
mediaDetails.poster_path
? `${tmdbImgEndPoint}${mediaDetails.poster_path}`
: posterPlaceholder
}
alt={mediaDetails.title}
className={`${
mediaDetails.backdrop_path ? "-mt-28" : "mt-2"
} mb-4 rounded`}
/>
{username && <Controls />}
</div>
{/* title and overview and links */}
<div className="col-span-12 md:col-span-9 ms-0 md:ms-4 p-0 md:p-8 flex flex-col justify-between">
<div className="mb-8 md:mb-0">
<h1 className="text-3xl font-normal">
{mediaType == "movie" ? mediaDetails.title : mediaDetails.name}
</h1>
{mediaDetails.overview && (
<p className="text-textLight text-[1.4rem] mt-6 hidden md:block">
{mediaDetails.overview}
</p>
)}
</div>
{/* Links */}
<ul className="flex justify-around text-xl" id="pagenav">
{routes.map((route) => (
<Link
className="hover:text-actionPrimary"
to={`/${mediaType}/${mediaid}/${
route.path === "/" ? "" : route.path
}`}
key={route.title}
>
{route.title}
</Link>
))}
</ul>
</div>
</div>
</div>
);
};
Redux:
export type ExtraMediaDetails = { mediaType: mediaTypeType; mediaid: number };
export type MediaDetailState =
| (MovieDetail & ExtraMediaDetails)
| (TvDetail & ExtraMediaDetails)
| {};
let initialState: MediaDetailState = {};
export const mediaSlice = createSlice({
name: "media",
initialState,
reducers: {
setDetails: (state, action: PayloadAction<MediaDetailState>) => {
Object.keys(state).forEach(
(key) => delete state[key as keyof MediaDetailState]
);
for (const key in action.payload) {
state[key as keyof MediaDetailState] =
action.payload[key as keyof MediaDetailState];
}
},
removeDetails: (state) => {
Object.keys(state).forEach(
(key) => delete state[key as keyof MediaDetailState]
);
},
},
});
Here are rest of the types:
type MediaDetailBase = {
adult: boolean;
backdrop_path: string;
homepage: string;
id: number;
original_language: string;
overview: string;
popularity: number;
poster_path: string;
production_companies: ProductionCompany[];
production_countries: ProductionCountry[];
spoken_languages: Language[];
status: string;
tagline: string;
vote_average: number;
vote_count: number;
};
export type MovieDetail = MediaDetailBase & {
belongs_to_collection: string;
budget: number;
genre_ids: MediaDetailGenre[];
imdb_id: string;
original_title: string;
release_date: string;
revenue: number;
runtime: number;
title: string;
video: boolean;
};
export type TvDetail = MediaDetailBase & {
created_by: TvCreator[];
episode_run_time: number[];
first_air_date: string;
genres: MediaDetailGenre[];
in_production: boolean;
languages: string[];
last_air_date: string;
last_episode_to_air: Episode;
name: string;
next_episode_to_air: string;
networks: Network[];
number_of_episodes: number;
number_of_seasons: number;
origin_country: string[];
original_name: string;
seasons: Season[];
type: string;
};
I was trying to use redux with typescript I initialised my state as {} and later it is to be populated
but ts is showing error that the state might be {} while fetching it from useSelector()
It is the expected behavior. I want to know to setup redux with ts so that these things don't happen
You could make the initial state include mediaType
and mediaid
as optional. Something like {} & Partial<ExtraMediaDetails>
.
export type ExtraMediaDetails = {
mediaType: mediaTypeType;
mediaid: number
};
export type MediaDetailState =
| (MovieDetail & ExtraMediaDetails)
| (TvDetail & ExtraMediaDetails)
| ({} & Partial<ExtraMediaDetails>);
ExtraMediaDetails
is indeed of the type{ mediaType: mediaTypeType; mediaid: number };
Butmediaid
andmediaType
are not the only properties that I am trying to extract from the state there are other properties such astitle
,poster_path
which are defined inMovieDetail
orTvDetail
but not inExtraMediaDetails
For this you'll need to test for which type you have at runtime. I often suggest adding a type
property based on generics.
Example:
type MediaDetailBase<T extends string> = {
type: T;
adult: boolean;
backdrop_path: string;
homepage: string;
id: number;
original_language: string;
overview: string;
popularity: number;
poster_path: string;
production_companies: ProductionCompany[];
production_countries: ProductionCountry[];
spoken_languages: Language[];
status: string;
tagline: string;
vote_average: number;
vote_count: number;
};
Then type the MovieDetail
and TvDetail
types:
export type MovieDetail = MediaDetailBase<"Movie"> & {
belongs_to_collection: string;
budget: number;
genre_ids: MediaDetailGenre[];
imdb_id: string;
original_title: string;
release_date: string;
revenue: number;
runtime: number;
title: string;
video: boolean;
};
export type TvDetail = MediaDetailBase<"TV"> & {
created_by: TvCreator[];
episode_run_time: number[];
first_air_date: string;
genres: MediaDetailGenre[];
in_production: boolean;
languages: string[];
last_air_date: string;
last_episode_to_air: Episode;
name: string;
next_episode_to_air: string;
networks: Network[];
number_of_episodes: number;
number_of_seasons: number;
origin_country: string[];
original_name: string;
seasons: Season[];
type: string;
};
I think MediaDetailState
can be simplified just a tad to:
export type MediaDetailState =
| ((MovieDetail | TvDetail) & ExtraMediaDetails)
| {};
And in your UI code where you access the state.media
value check the type
property and cast to the appropriate type to access the correct runtime properties.
Example:
const mediaDetails: MediaDetailState = useAppSelector((state) => state.media);
if ("type" in mediaDetails) {
switch (mediaDetails.type) {
case "Movie": {
// Cast as MovieDetail & ExtraMediaDetails
const {
mediaType,
mediaid,
// Other MovieDetail properties
} = mediaDetails as MovieDetail & ExtraMediaDetails;
break;
}
case "TV": {
// Cast as TvDetail & ExtraMediaDetails
const {
mediaType,
mediaid,
// Other TvDetail properties
} = mediaDetails as TvDetail & ExtraMediaDetails;
break;
}
}
} else {
// just the empty object
}