Блог

2025 / piscodev / Игорь Быстрицкий

обложка статьи
Блог

Типизация страницы в Next

Используй TypeScript и Next вместе, чтобы создать кастомный тип TNextPage, с помощью которого можно с легкостью указать типы params и searchParams на любой странице.

    • NextTypeScript
  • Сложно

  • 20 июня 2024
  • Игорь Быстрицкий

СложностьЗачем типизировать страницы?ТипизацияParamsДженерики P и SPSearchParamsРезультат
Если понравилась статья, поделитесь с друзьями

TypeScript / Больше статей

Cookie в Next

Cookie в Next

Как создавать и читать Cookie. Функции-обертки на TypeScript с удобной типизацией.

01 февраля 2024
Debounce. Оптимизация React приложения

Debounce. Оптимизация React приложения

Debounce — это разовый вызов функции после "этапа успокоения". Узнайте, как этот метод позволяет оптимизировать приложение.

24 декабря 2023

Сложность

Next

  • App Router

TypeScript

  • Дженерики
  • Записи (Record)

Зачем типизировать страницы?

С помощью TypeScript ты сможешь явно указать все возможные параметры, используемые в URL страницы. Например:

Bash

website.com/blog?category=programming&tag=react

Из этого URL ты можешь понять, что тебе нужно читать значения двух поисковых параметров: category и tag. Также видно, что эта страница не имеет динамического пути.

Примерный тип этой страницы будет такой:

TypeScript JSX

// Внимание на TNextPage.
type TBlogPage = TNextPage<never, ['tag', 'difficulty']>

Используя этот тип, ты получишь подсказки в редакторе (я надеюсь, это VSCode) и увидишь ошибки, если случайно обратишься к несуществующему параметру.

Типизация

Сначала давай рассмотрим возможности, которые предоставляет Next и поговорим о странице.

Напоминаю, что страницей называется файл page.tsx, который обязан экспортировать реакт компонент, имя которому ты придумываешь сам. Возьмем в качестве примера страницу новостей:

TypeScript JSX

// page.tsx

export default function NewsPage() {
	return <main>...</main>
}

Страница способна предоставить доступ к params и searchParams, которые были переданы как часть URL страницы.

Params

Параметры - это последнее значение в пути до динамической страницы. В случае Next такая страница может находиться по пути app/user/[id]/page.tsx. В данном случае [id] является динамическим параметром и доступен на странице page.tsx.

Получить его на странице можно следующим способом:

TypeScript JSX

// page.tsx

export default function UserPage(pageProps) {
	return <main>{pageProps.params.id}</main>
}

Однако в TypeScript такое не сработает, потому что VSCode не знает о типе pageProps, поэтому у тебя нет выбора - нужно типизировать. И как ты это собираешься делать - уже новый вопрос.

Можно пойти по простому пути и типизировать объектом напрямую:

TypeScript JSX

// page.tsx

export default function UserPage(pageProps: { params: { id: string } }) {
	const id = pageProps.params.id
	// id = 5
	return <main>...</main>
}

Вау, это сработало! Но из-за того, что тебе надо повторять эту запись на каждой новой странице, придется позаботиться о будущем и вынести эту запись в отдельный тип.

TypeScript JSX

// utils/index.ts

export type TNextPage = {
	params: { id: string }
}

// user/[id]/page.tsx

export default function UserPage(pageProps: TNextPage) {
	return <main>{pageProps.params.id}</main>
}

Дженерики P и SP

Просто супер! Но вот новая проблема: не все динамические страницы имеют путь с названием /[id]/, также может быть и /[slug]/, а может ты стремишься использовать внятные имена в проекте, поэтому укажешь в пути /[userId]/. В таком случае нужно типизировать уже с помощью дженерика и перезаписывать стандартный id.

TypeScript JSX

// utils/index.ts
// Дженерик 'P' и его стандартное значение 'id', также нужно ограничить его тип до строки используя ключ. слово 'extends', потому что все, что находится в URL удобнее обрабатывать как строку.
export type TNextPage<P extends string = 'id'> = {
	params: Record<P, string>
}

// Тут без изменений, дженерик передавать не нужно, потому что мы используем стандартное имя 'id'.
// user/[id]/page.tsx
export default function UserPage(pageProps: TNextPage) {
	return <main>{pageProps.params.id}</main>
}

// А тут нужно перезаписать с 'id' на 'articleSlug'.
// blog/[articleSlug]/page.tsx
// blog/kak-stat-krutim
export default function UserPage(pageProps: TNextPage<'articleSlug'>) {
	const slug = pageProps.params.articleSlug
	// slug = 'kak-stat-krutim'
	return <main>...</main>
}

SearchParams

С Params закончили, теперь к давай самому сложному и противному - поисковые параметры (далее: ПП).

В URL страницы он может быть один:

Bash

website.com/blog?tag=next

Может отсутствовать (оба tag и difficulty будут null):

Bash

website.com/blog
website.com/blog?

А может быть и несколько:

Bash

website.com/blog?tag=next&difficulty=easy

В последнем примере URL использует ? чтобы проверить наличие страницы blog и применить ПП. Каждый ПП имеет структуру ключ=значение и соединяется в цепочку с помощью &. Также важно знать, что ПП Next отдает тебе в виде объекта с ключами и значениями, поэтому его ПП имеет следующую форму:

TypeScript

const searchParams = {
	tag: 'next',
	difficulty: 'easy',
	...
}

Напомню, что ПП может быть много, поэтому можно легко забыть, какие ПП страница должна использовать, а также легко допустить опечатку и приложение сломается (извечная проблема JavaScript, поздравляю, если ты все еще не перешел на TypeScript).

Для типизации ПП нужно также использовать дженерик, чтобы указать их набор для каждой отдельной страницы. Дженерик будет просить массив строк, каждая из которых представляет имя каждого из возможных ПП. А теперь иди дописывай тип TNextPage новым дженериком.

TypeScript

export type TNextPage<P extends string = 'id', SP extends string[] = []> = {
	params: Record<P, string>
	searchParams: Record<SearchParams[number], string | null>
}
  • 2-ой дженерик SP ожидает массив строк, который изначально пуст. Очень удобно.
  • Сам searchParams представляет собой объект, в котором ключом выступает каждое значение из объединения, а значением является строка или ее отстутсивие. Это возможно благодаря шаблонной записи SearchParams[number], которая подразумевает, что из массива ты вытаскиваешь значение по индексу, но не указываешь его конкретно. Это можно назвать шаблонным перебором списка или как-то так. Благодаря этому трюку можно получить типизированный объект searchParams.

Результат

Теперь ты можешь применить этот тип к какой-нибудь странице.

TypeScript JSX

// app/blog/page.tsx

import { TNextPage } from '@/utils'

// Тип never, потому что страница не динамическая. Также нужно передать tag и difficulty в качестве ПП.
export type TBlogPage = TNextPage<never, ['tag', 'difficulty']>

export default function BlogPage({ params, searchParams }: TBlogPage) {
	const id = params.id // Ошибка: нельзя обращаться к never.
	const { tag, difficulty } = searchParams
	// tag = "next"
	// difficulty = "hard"
}

Специальный тип TNextPage делает работу с params и searchParams немного проще, ты также видишь подсказки от VSCode и TypeScript, в качестве бонуса твой проект откажется собираться, если увидит опечатку в коде. В одной из следующих статей я расскажу тебе, как написать кастомный хук для удобного управления searchParams в текущем URL. Этот хук будет очень простым, но от того не менее полезным, а также он будет использовать сегодняшний TNextPage.