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.
- You can store interfaces on the main file that uses them directly.
- 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.jsimport { 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 Moviesasync 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 {TitlePosterGenreDirector}}idslugmoviePoster {heightwidthurl}}}`,}),});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;};}) => (<MovieCardkey={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 movies
map function.
Zooming in on our Movie interface definition
// Movie TypeScript interface definitioninterface 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 id
, federateMovie
, slug
, 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 Title
, Poster
, alt
, Genre
, and Director
The moviePoster
property is also an object that contains height
, width
, 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 definitioninterface 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<MovieCardkey={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.tsimport { 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<MovieCardkey={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 SlackBlog Author