Типы для статических ресурсов

В проектах на JavaScript и TypeScript статическими ресурсами считаются файлы, которые не являются исполняемым кодом:

  • изображения;
  • SVG;
  • шрифты;
  • видео;
  • аудио;
  • CSS-файлы;
  • JSON;
  • WebAssembly-модули;
  • текстовые файлы;
  • markdown-документы.

Vite умеет импортировать такие ресурсы напрямую:

import logo fr om './logo.png'
import styles fr om './style.css'
import workerUrl from './worker.js?worker'

Во время сборки Vite преобразует подобные импорты в специальные URL, модули или объекты. Однако TypeScript изначально не знает, как интерпретировать такие файлы. Без дополнительных деклараций появляются ошибки:

Cannot find module './logo.png'

Для устранения подобных ошибок используются типы статических ресурсов.


Встроенные типы Vite

Vite поставляется со встроенным набором деклараций типов для большинства популярных форматов файлов. Подключаются они через пакет:

vite/client

Обычно это делается в файле:

vite-env.d.ts

Пример:

/// <reference types="vite/client" />

После подключения становятся доступны типы для:

  • PNG;
  • JPG;
  • SVG;
  • GIF;
  • WEBP;
  • AVIF;
  • CSS Modules;
  • import.meta.env;
  • worker-модулей;
  • raw-импортов;
  • URL-импортов.

Файл vite-env.d.ts

Vite рекомендует хранить пользовательские декларации типов в отдельном файле:

src/vite-env.d.ts

Пример структуры проекта:

src/
├── assets/
├── components/
├── vite-env.d.ts
└── main.ts

Содержимое:

/// <reference types="vite/client" />

Этот файл автоматически подхватывается TypeScript, если входит в область действия tsconfig.json.


Типизация изображений

PNG

Импорт PNG-файла:

import logo from './logo.png'

Тип переменной:

string

После сборки значение превращается в URL:

'/assets/logo.a1b2c3.png'

Пример использования:

const image = document.createElement('img')

image.src = logo

document.body.append(image)

JPG и JPEG

import photo from './photo.jpg'

или:

import photo from './photo.jpeg'

Тип:

string

GIF

import animation from './animation.gif'

Тип:

string

WEBP

import banner from './banner.webp'

AVIF

import image from './image.avif'

SVG как URL

По умолчанию SVG импортируется как URL:

import icon from './icon.svg'

Тип:

string

Пример:

const img = document.createElement('img')

img.src = icon

SVG как компонент

Во многих проектах SVG импортируется как React-компонент через плагины:

import Logo from './logo.svg'

Без дополнительных типов TypeScript выдаст ошибку.

Для корректной типизации создаётся декларация:

declare module '*.svg' {
  import * as React from 'react'

  const ReactComponent: React.FC<
    React.SVGProps<SVGSVGElement>
  >

  export default ReactComponent
}

Теперь SVG можно использовать как компонент:

<Logo width={120} height={120} />

Типизация CSS Modules

Vite поддерживает CSS Modules из коробки.

Файл:

Button.module.css

Импорт:

import styles from './Button.module.css'

Тип объекта:

{
  readonly [key: string]: string
}

Использование:

button.className = styles.primary

Автоматическая генерация типов CSS Modules

По умолчанию TypeScript знает только о строковых ключах:

styles.primary
styles.secondary

Но конкретные имена классов неизвестны.

Для строгой типизации применяются генераторы:

  • typed-css-modules;
  • vite-plugin-dts;
  • vite-plugin-sass-dts;
  • typed-scss-modules.

Пример автоматически созданного файла:

declare const styles: {
  readonly primary: string
  readonly secondary: string
}

export default styles

Теперь TypeScript проверяет корректность классов:

styles.primary

Ошибка:

styles.primray

Импорт CSS без модулей

Обычный CSS импортируется как side effect:

import './global.css'

Значение не возвращается.

Типизация не требуется.


Типы для SCSS и Sass

Vite поддерживает:

  • .scss;
  • .sass.

Пример:

import styles from './layout.module.scss'

Тип аналогичен CSS Modules:

{
  readonly [key: string]: string
}

Типы для Less

import styles from './theme.module.less'

Типы для Stylus

import styles from './app.module.styl'

Импорт JSON

Vite поддерживает JSON напрямую.

import config from './config.json'

TypeScript автоматически выводит тип:

{
  api: string
  timeout: number
}

Пример:

{
  "api": "/api",
  "timeout": 5000
}

Использование:

console.log(config.api)

resolveJsonModule

Для работы JSON должен быть включён параметр:

{
  "compilerOptions": {
    "resolveJsonModule": true
  }
}

В современных шаблонах Vite он обычно уже активирован.


Импорт raw-файлов

Специальный суффикс:

?raw

Позволяет импортировать содержимое файла как строку.

Пример:

import text from './article.md?raw'

Тип:

string

Использование:

console.log(text)

Импорт URL

Суффикс:

?url

Пример:

import fileUrl from './document.pdf?url'

Тип:

string

После сборки:

'/assets/document.123abc.pdf'

Импорт worker-модулей

Vite поддерживает Web Workers.

Worker как модуль

import Worker from './worker.ts?worker'

Тип:

WorkerConstructor

Использование:

const worker = new Worker()

Inline worker

import Worker from './worker.ts?worker&inline'

Shared Worker

import SharedWorker from './worker.ts?sharedworker'

Типы для WebAssembly

Импорт:

import init from './module.wasm'

Тип обычно:

string

или специальный loader-тип, если используется плагин.


Пользовательские декларации модулей

Если TypeScript не знает о формате файла, создаётся собственная декларация.

Пример для .txt:

declare module '*.txt' {
  const content: string

  export default content
}

Теперь можно писать:

import text from './notes.txt'

Типизация markdown-файлов

Импорт как строка

declare module '*.md' {
  const content: string

  export default content
}

Импорт markdown как HTML

Некоторые плагины преобразуют markdown в HTML:

declare module '*.md' {
  const html: string

  export default html
}

Markdown как объект

Иногда markdown-файл экспортирует структуру:

declare module '*.md' {
  export const attributes: Record<string, string>

  const body: string

  export default body
}

Пользовательские бинарные форматы

Пример:

declare module '*.bin' {
  const file: ArrayBuffer

  export default file
}

Глобальные декларации

Типы ресурсов обычно размещаются в:

src/types/

или:

src/@types/

Пример:

src/
├── @types/
│   ├── assets.d.ts
│   └── markdown.d.ts

include в tsconfig.json

TypeScript должен видеть декларации:

{
  "include": [
    "src",
    "src/@types"
  ]
}

exclude и проблемы типизации

Иногда декларации не работают из-за неправильного exclude:

{
  "exclude": [
    "src/types"
  ]
}

В таком случае TypeScript игнорирует объявления модулей.


declare module

Основной механизм типизации статических ресурсов:

declare module '*.ext'

Пример:

declare module '*.shader' {
  const shader: string

  export default shader
}

Несколько экспортов

Модуль может экспортировать несколько значений:

declare module '*.data' {
  export const version: string

  const content: string

  export default content
}

Типизация import.meta.glob

Vite поддерживает динамический импорт групп файлов.

Пример:

const modules = import.meta.glob('./pages/*.ts')

Тип:

Record<
  string,
  () => Promise<unknown>
>

Типизация import.meta.glob с generic

Можно явно указать тип модуля:

const pages = import.meta.glob<{
  default: string
}>('./content/*.md')

Теперь TypeScript знает структуру импорта.


eager-режим

const modules = import.meta.glob(
  './modules/*.ts',
  { eager: true }
)

Тип:

Record<string, unknown>

С generic:

const modules = import.meta.glob<{
  setup(): void
}>('./modules/*.ts', {
  eager: true
})

Типизация import.meta.glob для компонентов

Пример React-компонентов:

const pages = import.meta.glob<{
  default: React.ComponentType
}>('./pages/*.tsx')

Типизация import.meta.glob для markdown

const articles = import.meta.glob<{
  metadata: {
    title: string
    date: string
  }

  default: string
}>('./articles/*.md')

Типизация import.meta.globEager

В старых версиях Vite использовался:

import.meta.globEager()

В новых версиях предпочтителен:

import.meta.glob('*', {
  eager: true
})

Типы import assertions

Современный JavaScript поддерживает import assertions.

Пример:

import config from './config.json' assert {
  type: 'json'
}

TypeScript использует встроенные JSON-типы.


Asset Inline Lim it

Маленькие файлы могут встраиваться как base64.

Пример:

import icon from './icon.png'

После сборки:

data:image/png;base64,...

Тип остаётся:

string

new URL и типизация

Vite поддерживает:

new URL('./image.png', import.meta.url)

Тип:

URL

Пример:

const imageUrl = new URL(
  './image.png',
  import.meta.url
).href

Типизация env-ресурсов

Файл:

VITE_API_URL=https://api.site.com

Использование:

import.meta.env.VITE_API_URL

Типизация:

interface ImportMetaEnv {
  readonly VITE_API_URL: string
}

Расширение ImportMeta

interface ImportMeta {
  readonly env: ImportMetaEnv
}

Полный пример vite-env.d.ts

/// <reference types="vite/client" />

declare module '*.svg' {
  const src: string

  export default src
}

declare module '*.md' {
  const content: string

  export default content
}

interface ImportMetaEnv {
  readonly VITE_API_URL: string
  readonly VITE_APP_TITLE: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

Распространённые ошибки

Cannot find module

Причины:

  • отсутствует декларация;
  • файл не входит в include;
  • неверное расширение;
  • отсутствует vite/client.

Duplicate identifier

Появляется при дублировании деклараций:

declare module '*.svg'

в нескольких файлах одновременно.


Property does not exist

Ошибка:

styles.primary

Причина:

  • отсутствует генерация типов CSS Modules;
  • неправильный импорт;
  • устаревший d.ts-файл.

Практика организации типов ресурсов

Крупные проекты обычно разделяют декларации:

src/@types/
├── assets.d.ts
├── css.d.ts
├── env.d.ts
├── markdown.d.ts
└── workers.d.ts

Пример assets.d.ts:

declare module '*.png' {
  const src: string

  export default src
}

declare module '*.jpg' {
  const src: string

  export default src
}

declare module '*.webp' {
  const src: string

  export default src
}

Типизация ресурсов в монорепозиториях

В монорепозиториях декларации часто выносятся в отдельный пакет:

packages/
├── types/
├── ui/
└── app/

Пакет типов:

@company/types-vite

Подключение:

{
  "compilerOptions": {
    "types": [
      "@company/types-vite"
    ]
  }
}

Совместимость с Node.js

Некоторые ресурсы доступны только в браузере.

Например:

import image from './image.png'

В Node.js такой импорт без bundler-а не работает.

Поэтому типизация Vite относится именно к среде сборщика, а не к стандартному TypeScript-runtime.


Роль esbuild в обработке ресурсов

Во время dev-сервера Vite использует:

esbuild

Он обрабатывает импорты ресурсов и преобразует их в браузерно-совместимый формат.

Во время production-сборки применяется:

Rollup

Типы при этом остаются исключительно задачей TypeScript и не влияют на итоговый JavaScript-код.