Here's a quick summary of everything we released in Q1 2024.

Everything in its right place: How to abstract types in TypeScript

TypeScript (Ts) is a strongly typed programming language that compiles to JavaScript, so it can used anywhere JavaScript (Js) is used, such as a browser. Learn when and how to abstract your types and interfaces when using Typescript.
Lo Etheridge

Lo Etheridge

Feb 12, 2024
How to abstract types in TypeScript

TypeScript (Ts) is a strongly typed programming language that compiles to JavaScript, so it can be used anywhere JavaScript (Js) is used, such as a browser. These qualities and the ability to work at any scale of application or tooling make it ideal for integrating a front-end with a CMS. You might be thinking, if it runs like Javascript and converts to it in the end, why not use JavaScript? The magic of TypeScript is that it adds strictly defined syntax called a type system that creates a tighter connection with your CMS and helps you catch errors faster and earlier.

When using TypeScript, you can organize your interfaces and types in various ways.

  1. You can store interfaces on the main file that uses them directly.
  2. You can explicitly export and import types from .ts files like any other class, function, or object. These files may be TypeScript files that only contain types. You can keep these files in the root folder or locally in the specific directory.

#When NOT to abstract your types in TypeScript

When you’re new to Typescript, keeping your types close at hand can give you a sense of security. It also keeps you from over-optimizing too early. When you have your first set of types, you can write them in place, and when you need to use them again, abstract them into a new file. When you’ve architected a few projects like this, you may already know when you’ll want to do this and abstract things from the beginning.

#When to Abstract your types in TypeScript

When you’re new to Typescript, keeping your types close at hand can give you a sense of security. It also keeps you from over-optimizing too early. When you have your first set of types, you can write them in place, and when you need to use them again, abstract them into a new file. When you’ve architected a few projects like this, you may already know when you’ll want to do this and abstract things from the beginning.

Looking at a TypeScript example: Hygraphlix

In the first iteration of Hygraphlix, a movie streaming platform demo built with Hygraph and NextJS 14, I placed my types and interfaces in the main file where they were used. I am still somewhat new to TypeScript, and having my types in the same place as the rest of my code helped me understand their connection to the Hygraph CMS and my NextJS code.

The code below creates the Hygraphlix Homepage that features the Top 8 movies on the platform by querying the first eight movies from Hygraph.

//Homepage with movies: app/page.js
import { Link } from "@nextui-org/link";
import { Snippet } from "@nextui-org/snippet";
import { Code } from "@nextui-org/code";
import { button as buttonStyles } from "@nextui-org/theme";
import { siteConfig } from "@/config/site";
import { title, subtitle } from "@/components/primitives";
import { GithubIcon } from "@/components/icons";
import MovieCard from "@/components/MovieCard";
//Get featured Movies
async function getFeaturedMovies() {
const HYGRAPH_ENDPOINT = process.env.HYGRAPH_ENDPOINT;
if (!HYGRAPH_ENDPOINT) {
throw new Error("HYGRAPH_ENDPOINT is not defined");
}
const response = await fetch(HYGRAPH_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
query Movies {
movies(first: 8) {
federateMovie {
data {
Title
Poster
Genre
Director
}
}
id
slug
moviePoster {
height
width
url
}
}
}`,
}),
});
const json = await response.json();
return json.data.movies;
}

The query returns our movie data in JSON format, and we map over it to place each movie in a MovieCard component:

export default async function Home() {
const movies = await getFeaturedMovies();
//console.log(movies);
return (
<>
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<!-- Homepage markup/HMTL here... -->
<div className="grid px-5 mt-4 lg:gap-xl-12 gap-x-6 md:grid-cols-2 lg:grid-cols-4">
{movies.map(
// Declare movie interface and map over movies list to render in movie cards
(movie: {
id: string;
federateMovie: {
data: {
Title: string;
Poster: string;
alt: string;
Genre: string;
Director: string;
};
};
slug: string;
moviePoster: {
height: number;
width: number;
url: string;
};
}) => (
<MovieCard
key={movie.id}
Title={movie.federateMovie.data.Title}
Poster={movie.federateMovie.data.Poster}
moviePoster={movie.moviePoster}
alt={movie.federateMovie.data.Title}
Genre={movie.federateMovie.data.Genre}
Director={movie.federateMovie.data.Director}
slug={movie.slug}
/>
)
)}
</div>
</section>
</div>
</>
);
}

The async Home function retrieves all the featured movie data and maps them to a MovieCard component. The TypeScript Movie interface definition is located inside of the moviesmap function.

Zooming in on our Movie interface definition

// Movie TypeScript interface definition
interface Movie {
id: string;
federateMovie: {
data: {
Title: string;
Poster: string;
alt: string;
Genre: string;
Director: string;
};
};
slug: string;
moviePoster: {
height: number;
width: number;
url: string;
};
}

In the Movie Interface definition, we are defining the Movie object and properties like idfederateMovieslug, and moviePoster. The federateMovie object is a remote source coming into Hygraph from OMDB, and open-source movie database that contains a data object with properties like TitlePosteraltGenre, and Director The moviePoster property is also an object that contains heightwidth, and url.

#Abstracting your types and interfaces into the same file

You can get started quickly with abstracting by declaring your types in the file where they will be used, but not directly in your function. Looking at the Homepage code again, you could declare the Movie interface above the async function Home() , like so:

// Movie TypeScript interface definition
interface Movie {
id: string;
federateMovie: {
data: {
Title: string;
Poster: string;
alt: string;
Genre: string;
Director: string;
};
};
slug: string;
moviePoster: {
height: number;
width: number;
url: string;
};
}
export default async function Home() {
const movies: Movie[] = await getFeaturedMovies();
//console.log(movies);
return (
<>
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<!-- Homepage markup/HMTL here... -->
<div className="grid px-5 mt-4 lg:gap-xl-12 gap-x-6 md:grid-cols-2 lg:grid-cols-4">
{movies.map((movie: Movie) => (
// Map over movie object list to render in movie cards
<MovieCard
key={movie.id}
Title={movie.federateMovie.data.Title}
Poster={movie.federateMovie.data.Poster}
moviePoster={movie.moviePoster}
alt={movie.federateMovie.data.Title}
Genre={movie.federateMovie.data.Genre}
Director={movie.federateMovie.data.Director}
slug={movie.slug}
/>
)
)}
</div>
</section>
</div>
</>
);
}

In this location and in line with the function, it is easier to understand how the Movie interface helps ensure that components receive the correct props and that objects have the correct structure as it strictly defines the shape of our query result or data object and the props that will be used in our Next app. This can help catch errors at compile time rather than runtime, making the code more robust and easier to debug.

While putting your types and interfaces directly into the file where they are used is okay, what happens when you want to use the Movie object again? You would have to define it in every new file that uses it. This can get complicated and messy very quickly when you have a large application. Another way to organize your types and interfaces when using TypeScript is to have a file that contains all of our definitions and import it into the files that require it. So, let’s do that!

#Abstracting your types and interfaces into a single file

To put all the types used in the Hygraphlix Next app together, go to the folder called types in the root of the project and rename the index.ts file to types.ts and copy and paste the types from the app routes and component directories. When complete, the file should match the below code:

// @/types/types.ts
import { SVGProps } from "react";
import { ThemeProviderProps } from "next-themes/dist/types";
export interface Movie {
id: string;
federateMovie: {
data: {
Title: string;
Poster: string;
alt: string;
Genre: string;
Director: string;
};
};
slug: string;
moviePoster: {
height: number;
width: number;
url: string;
};
}
export interface MuxPlayerProps {
playbackId: string;
}
export type MovieHeroProps = {
Title: string;
//Poster: string;
Plot: string;
Actors: string;
Director: string;
Genre: string;
Rated: string;
Runtime: string;
Year: string;
};
export type MovieCardProps = {
Poster: string;
alt: string;
Title: string;
Genre: string;
Director: string;
slug: string;
moviePoster: {
height: number;
width: number;
url: string;
};
};
export type IconSvgProps = SVGProps<SVGSVGElement> & {
size?: number;
};
export interface ProvidersProps {
children: React.ReactNode;
themeProps?: ThemeProviderProps;
}

The addition of export allows our types and interfaces to be used throughout our Next app as needed.

Importing types into the homepage

Now that our types are in one file let’s take a quick look at the Homepage code from earlier to import our Movie interface:

import { Movie } from "@/types/types";
export default async function Home() {
const movies: Movie[] = await getFeaturedMovies();
//console.log(movies);
return (
<>
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<!-- Homepage markup/HMTL here... -->
<div className="grid px-5 mt-4 lg:gap-xl-12 gap-x-6 md:grid-cols-2 lg:grid-cols-4">
{movies.map((movie: Movie) => (
// Map over movie object list to render in movie cards
<MovieCard
key={movie.id}
Title={movie.federateMovie.data.Title}
Poster={movie.federateMovie.data.Poster}
moviePoster={movie.moviePoster}
alt={movie.federateMovie.data.Title}
Genre={movie.federateMovie.data.Genre}
Director={movie.federateMovie.data.Director}
slug={movie.slug}
/>
)
)}
</div>
</section>
</div>
</>
);
}

In the above code, we are now importing the Movie type from the types.ts file. Our Home function awaits the result of the getFeaturedMovies function and assigns it to the movies variable that contains the Movie object that we defined in our type definitions. We map over the movies array and pass the necessary props from our Movie object to render the list of movies. Not only is our homepage code a bit cleaner, but we have also removed the redundancy of having to define our types every time they are used in a new file. Let’s say, for example, we want to create a ‘related movie’ area on each individual movie page; by importing the types, we get the proper shape of Movie object without having to rewrite all the code.

#Conclusion

There are many approaches and opinions about where you should put your types when using Typescript. Some developers even autogenerate their types, while some prefer placing their types in the route folder where they will be used rather than one file at the root level. Ultimately, what matters is your own comfort level with your code and making sure your code is readable to the developer who comes along after you have moved on to another project. Clone the Hygraphlix project and frontend code and practice separating the types. To view Hygraphlix with all types stored in types.ts, go to the abstract-types branch of the repo. If you have any questions or feedback, find me in the community!

Join the community

Need help? Want to show off? Join the Slack community and meet other developers building with Hygraph

Meet us in Slack

Blog Author

Lo Etheridge

Lo Etheridge

Senior Developer Relations

Lo is Hygraph's Senior Developer Relations Specialist. They focus on developer education around Headless CMS, JS-based frontends, and API technologies as well as the dynamic cross-functional relationship between developers and content professionals.

Share with others

Sign up for our newsletter!

Be the first to know about releases and industry news and insights.