I have a variable named movies
that can take the form of two shapes defined as the following interfaces:
interface BaseMovie {
id: number;
title: string;
}
interface MultiSearchResult extends BaseMovie {
media_type: "movie" | "tv" | "person";
}
I know that when another variable, triggeredSearch
is true, the shape will be MultiSearchResult
. The shape will be BaseMovie
when triggeredSearch
is false.
While using this to render each item
<ul>
{movies.map((movie, i) => (
<li key={i}>{!triggeredSearch ? movie.title : movie.media_type}</li>
))}
</ul>
I receive the following warning:
Property 'media_type' does not exist on type 'BaseMovie | MultiSearchResult'. Property 'media_type' does not exist on type 'BaseMovie'.ts(2339)
Is there a way, outside of using the as
keyword (i.e. (movie as MultiSearchResult).media_type
), to declare that a movie
is a MultiSearchResult
when triggeredSearch
is true?
A full example can be found here.
CodePudding user response:
TypeScript can't narrow the type of a variable based on the value of another variable, but it can narrow the type of a union based on one of the union's members. So if you can put triggeredSearch
and movies
in the same data structure, you can use a discriminated union to distinguish things:
type Example =
{ triggeredSearch: true, movies: null | MultiSearchResult[] }
|
{ triggeredSearch: false, movies: null | BaseMovie[] };
declare const data: Example;
// Side note: Using thea rray index as the key an anti-pattern if the
// arrays contents ever change, more here:
// https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318
const items = data.triggeredSearch
? data.movies?.map((movie, i) => <li key={i}>{movie.media_type}</li>)
: data.movies?.map((movie, i) => <li key={i}>{movie.title}</li>);
const result = <ul>{items}</ul>;
If you can't, you can use a type assertion function rather than as
, which has the advantage of giving you a runtime error if the assertion is false:
function assertMultiSearchResult(movie: BaseMovie): asserts movie is MultiSearchResult {
if (!("media_type" in movie)) {
throw new Error(`Given 'movie' is not a 'MultiSearchResult');
}
}
Then:
<ul>
{movies?.map((movie, i) => {
let content: string;
if (triggeredSerach) {
assertMultiSearchResult(movie);
content = movie.media_type;
} else {
content = movie.title;
}
return <li key={i}>{content}</li>;
})}
</ul>
More long-winded, but also more typesafe.
CodePudding user response:
Typescript doesn't understand your business context.
Why not just set the media_type
property to optional, and type your movies
state to MultiSearchResult[]
only?
import { useEffect, useState } from "react";
import "./styles.css";
interface BaseMovie {
id: number;
title: string;
}
interface MultiSearchResult extends BaseMovie {
media_type?: "movie" | "tv" | "person";
}
export default function App() {
const [triggeredSearch, setTriggeredSearch] = useState(false);
const [movies, setMovies] = useState<MultiSearchResult[]>([
{ id: 1, title: "Avengers" },
{ id: 2, title: "Eternals" }
]);
useEffect(() => {
setMovies([
{ id: 1, title: "Avengers", media_type: "movie" },
{ id: 2, title: "Eternals", media_type: "movie" }
]);
}, [triggeredSearch]);
return (
<div>
<button onClick={() => setTriggeredSearch(true)}>Trigger Search</button>
<ul>
{movies.map((movie, i) => (
<li key={i}>{!triggeredSearch ? movie.title : movie.media_type}</li>
))}
</ul>
</div>
);
}