Декларации типов для ресурсов (svg, css, images)

TypeScript изначально понимает только JavaScript/TypeScript-модули. При попытке импортировать SVG, PNG, CSS, SCSS или другие нестандартные ресурсы компилятор выдаёт ошибку:

Cannot find module './logo.svg'

Webpack способен обрабатывать подобные файлы через loaders, asset modules и плагины, однако TypeScript ничего не знает о механизме сборки. Для него такие импорты остаются неизвестными.

Проблема решается через декларации модулей (*.d.ts), которые объясняют TypeScript:

  • что представляет собой импортируемый файл;
  • какой тип возвращается;
  • можно ли импортировать файл как строку, объект или React-компонент;
  • какие поля существуют у CSS Modules.

Без деклараций:

  • IDE не понимает импорты;
  • не работает автодополнение;
  • TypeScript останавливает компиляцию;
  • невозможно безопасно использовать CSS Modules.

Как TypeScript обрабатывает нестандартные импорты

Когда встречается импорт:

import logo from './logo.svg';

TypeScript:

  1. пытается найти logo.svg.ts;
  2. ищет logo.svg.d.ts;
  3. анализирует node_modules;
  4. проверяет существующие декларации модулей.

Если подходящая декларация отсутствует — появляется ошибка.

Webpack здесь не участвует. Проверка выполняется ещё до сборки.


Структура деклараций типов

Обычно создаётся отдельный файл:

src/types/assets.d.ts

или:

global.d.ts

Внутри объявляются виртуальные модули.

Пример:

declare module '*.png' {
    const value: string;
    export default value;
}

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

import image from './image.png';

и знает, что image — строка.


Декларации для изображений

PNG

declare module '*.png' {
    const src: string;
    export default src;
}

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

import logo from './logo.png';

const img = document.createElement('img');

img.src = logo;

JPG и JPEG

declare module '*.jpg' {
    const src: string;
    export default src;
}

declare module '*.jpeg' {
    const src: string;
    export default src;
}

GIF

declare module '*.gif' {
    const src: string;
    export default src;
}

WebP

declare module '*.webp' {
    const src: string;
    export default src;
}

AVIF

declare module '*.avif' {
    const src: string;
    export default src;
}

ICO

declare module '*.ico' {
    const src: string;
    export default src;
}

Декларации для SVG

SVG — особый случай. Его можно использовать несколькими способами.


SVG как URL

Если Webpack обрабатывает SVG как asset/resource:

declare module '*.svg' {
    const src: string;
    export default src;
}

Импорт:

import icon from './icon.svg';

img.src = icon;

Webpack вернёт URL:

/assets/icon.a1b2c3.svg

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

При использовании @svgr/webpack SVG превращается в React-компонент.

Webpack:

{
    test: /\.svg$/,
    use: ['@svgr/webpack']
}

Декларация:

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

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

    const src: string;

    export default src;
}

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

import { ReactComponent as Logo } from './logo.svg';

export const Header = () => {
    return <Logo width={120} />;
};

SVG одновременно как URL и компонент

Современные конфигурации часто поддерживают оба варианта:

import iconUrl from './icon.svg';

import { ReactComponent as Icon } from './icon.svg';

В этом случае декларация должна содержать:

  • default export;
  • named export.

Полный вариант:

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

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

    const content: string;

    export default content;
}

Декларации для CSS

Обычный CSS

Если CSS импортируется ради побочного эффекта:

import './styles.css';

можно использовать пустую декларацию:

declare module '*.css';

Этого достаточно.


CSS как модуль

Если используется CSS Modules:

.button {
    color: red;
}

Импорт:

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

TypeScript должен понимать структуру объекта.

Минимальная декларация:

declare module '*.module.css' {
    const classes: {
        [key: string]: string;
    };

    export default classes;
}

Теперь доступно:

styles.button

Недостаток индексной сигнатуры

Конструкция:

[key: string]: string;

не обеспечивает строгую типизацию.

TypeScript разрешит:

styles.unknownClass

даже если класса не существует.


Генерация строгих типов CSS Modules

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

  • typed-css-modules;
  • css-modules-typescript-loader;
  • typed-scss-modules.

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

declare const styles: {
    readonly button: string;
    readonly active: string;
};

export default styles;

Теперь TypeScript обнаруживает ошибки:

styles.unknown;

Ошибка:

Property 'unknown' does not exist

Декларации для SCSS и SASS

Обычный SCSS

declare module '*.scss';

SCSS Modules

declare module '*.module.scss' {
    const classes: {
        [key: string]: string;
    };

    export default classes;
}

SASS

declare module '*.sass' {
    const classes: {
        [key: string]: string;
    };

    export default classes;
}

Декларации для LESS

Обычный LESS

declare module '*.less';

LESS Modules

declare module '*.module.less' {
    const classes: {
        [key: string]: string;
    };

    export default classes;
}

Декларации для файлов шрифтов

WOFF

declare module '*.woff' {
    const src: string;
    export default src;
}

WOFF2

declare module '*.woff2' {
    const src: string;
    export default src;
}

TTF

declare module '*.ttf' {
    const src: string;
    export default src;
}

EOT

declare module '*.eot' {
    const src: string;
    export default src;
}

Декларации для медиафайлов

MP4

declare module '*.mp4' {
    const src: string;
    export default src;
}

WebM

declare module '*.webm' {
    const src: string;
    export default src;
}

MP3

declare module '*.mp3' {
    const src: string;
    export default src;
}

WAV

declare module '*.wav' {
    const src: string;
    export default src;
}

Универсальная декларация ресурсов

Иногда создаётся единый файл:

declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.svg';
declare module '*.scss';
declare module '*.css';

Подход уменьшает объём кода, но имеет недостатки:

  • отсутствуют строгие типы;
  • ухудшается автодополнение;
  • нельзя описать React-компоненты SVG;
  • сложнее поддерживать проект.

Подключение деклараций

TypeScript должен видеть .d.ts файлы.

Обычно они автоматически включаются через:

{
    "include": [
        "src"
    ]
}

Если декларации лежат вне src:

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

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

Иногда применяется отдельная папка типов:

types/
    assets.d.ts

Конфигурация:

{
    "compilerOptions": {
        "typeRoots": [
            "./types",
            "./node_modules/@types"
        ]
    }
}

Разница между include и typeRoots

include

Добавляет файлы в программу TypeScript.

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

Подключаются:

  • .ts;
  • .tsx;
  • .d.ts.

typeRoots

Определяет директории поиска глобальных типов.

{
    "typeRoots": [
        "./types"
    ]
}

После указания typeRoots TypeScript перестаёт автоматически искать типы вне перечисленных директорий.

Это частая причина ошибок:

Cannot find type definition file

Глобальные декларации и модульная область видимости

Файл .d.ts может быть:

  • глобальным;
  • модульным.

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

declare module '*.png' {
    const src: string;
    export default src;
}

Работает глобально.


Модульная область

Если добавить:

export {};

файл становится модулем.

Некоторые глобальные декларации могут перестать работать.


Конфликт деклараций

Ошибка:

Duplicate identifier

возникает, когда:

  • одна и та же декларация объявлена несколько раз;
  • подключаются конфликтующие пакеты типов;
  • существуют дублирующие .d.ts.

Пример проблемы:

declare module '*.svg'

в двух разных файлах.


Связь деклараций с Webpack loaders

Типизация должна соответствовать реальному поведению Webpack.


Неверная типизация

Декларация:

declare module '*.svg' {
    const src: string;
    export default src;
}

Но loader возвращает React-компонент.

В результате:

  • TypeScript считает значение строкой;
  • во время выполнения приходит функция.

Это приводит к логическим ошибкам.


Asset Modules и типы

Webpack 5 заменил:

  • file-loader;
  • url-loader;
  • raw-loader.

Теперь используются:

  • asset/resource;
  • asset/inline;
  • asset/source;
  • asset.

asset/resource

Возвращает URL:

type: 'asset/resource'

Тип:

const src: string;

asset/inline

Возвращает base64:

data:image/png;base64,...

Тип также остаётся строкой:

const src: string;

asset/source

Возвращает содержимое файла.

Например:

type: 'asset/source'

Для SVG:

declare module '*.svg' {
    const content: string;
    export default content;
}

Но теперь строка содержит XML-код, а не URL.


Типизация raw-loader

Старый подход:

use: 'raw-loader'

Импорт:

import template from './template.html';

Декларация:

declare module '*.html' {
    const content: string;
    export default content;
}

Типизация JSON

TypeScript умеет работать с JSON нативно.

В tsconfig.json:

{
    "resolveJsonModule": true
}

После этого:

import data from './data.json';

работает без дополнительных деклараций.


Типизация WebAssembly

Для .wasm иногда используются декларации:

declare module '*.wasm' {
    const value: WebAssembly.Module;
    export default value;
}

или:

declare module '*.wasm' {
    const init: () => Promise<any>;
    export default init;
}

Тип зависит от конфигурации Webpack и wasm-loader.


Организация файлов типов

Распространённая структура:

src/
    types/
        assets.d.ts
        styles.d.ts
        svg.d.ts

Или:

types/
    global.d.ts

Разделение деклараций по категориям

assets.d.ts

declare module '*.png';
declare module '*.jpg';
declare module '*.gif';

styles.d.ts

declare module '*.module.scss' {
    const classes: {
        [key: string]: string;
    };

    export default classes;
}

svg.d.ts

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

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

    const src: string;

    export default src;
}

Практический пример конфигурации

Webpack

module.exports = {
    module: {
        rules: [
            {
                test: /\.module\.scss$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            modules: true
                        }
                    },
                    'sass-loader'
                ]
            },
            {
                test: /\.svg$/,
                use: [
                    '@svgr/webpack'
                ]
            },
            {
                test: /\.(png|jpg|gif)$/i,
                type: 'asset/resource'
            }
        ]
    }
};

declarations.d.ts

declare module '*.module.scss' {
    const classes: {
        [key: string]: string;
    };

    export default classes;
}

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

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

    const src: string;

    export default src;
}

declare module '*.png' {
    const src: string;
    export default src;
}

declare module '*.jpg' {
    const src: string;
    export default src;
}

declare module '*.gif' {
    const src: string;
    export default src;
}

Типичные ошибки

Cannot find module

Причины:

  • отсутствует .d.ts;
  • TypeScript не видит папку типов;
  • неправильный include;
  • файл не входит в tsconfig.

SVG импортируется неверно

Причина:

  • декларация не соответствует loader.

CSS Modules без автодополнения

Причина:

  • используется индексная сигнатура;
  • отсутствует генерация типов.

Duplicate identifier

Причины:

  • дубли деклараций;
  • пересечение глобальных типов;
  • несколько одинаковых .d.ts.

Практика крупных проектов

В production-проектах обычно:

  • отдельно типизируются SVG;
  • CSS Modules генерируют строгие .d.ts;
  • asset modules приводятся к единообразию;
  • типы ресурсов хранятся централизованно;
  • декларации разбиваются по категориям;
  • избегаются универсальные declare module '*'.

Строгая типизация ресурсов особенно важна в:

  • React-приложениях;
  • design systems;
  • monorepo;
  • SSR-проектах;
  • TypeScript-first архитектуре;
  • больших frontend-командах.