Home > Blockchain >  Conditional Type without using As
Conditional Type without using As

Time:12-04

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>;

Playground link

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>
  );
}

  • Related