Using SVGs with Next.js 11 and TypeScript

Problem: SVG TypeScript ESLint errors on any value

After updating to Next.js 11 (from v10.2.3 to v11.0.1) I noticed that TypeScript was having ESLint issues specifically related to SVGs.

Unsafe assignment of an `any` value.
(eslint@typescript-eslint/no-unsafe-assignment)

Unsafe member access .palette on an `any` value.
(eslint@typescript-eslint/no-unsafe-member-access)

The TypeScript ESLint errors were occurring on all previously working SVG components:

SomeComponent.tsx
import { default as Logo } from '~/public/logo.svg';

<Logo
  height={40}
  css={(theme) => ({        ^^^^^
        Parameter 'theme' implicitly has an 'any' type.ts(7006)

    fill: theme.palette.primary.main,    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    Unsafe assignment of an `any` value.
    (eslint@typescript-eslint/no-unsafe-assignment)
  })}
/>
SomeComponent.tsx
import { default as XIcon } from '~/public/icons/x.svg';

<IconButton
  component='a'
  aria-label='exit sign-up'
  size={{ xs: 32, md: 40 }}
  icon={XIcon}        ^^^^^
        Unsafe assignment of an `any` value.
        (eslint@typescript-eslint/no-unsafe-assignment)
/>

For some reason our module declarations for *.svg is no longer being recognized:

@types/index.d.ts
declare module "*.svg" {
  const component: React.FC<React.SVGProps<SVGSVGElement>>;

  export default component;
}

Reason

Next.js 11 introduced it's own image import types to prevent conflicts with existing image handling set-ups.

Unfortunately, these image import module declarations are included in the non-modifiable (and regenerated at every build) next-env.d.ts file:

  /// <reference types="next" />
  /// <reference types="next/types/global" />
+ /// <reference types="next/image-types/global" />

The newly included next/image-types/global.d.ts module declarations file is overwriting our own *.svg module declaration, and setting the type to any:

next.js/packages/next/image-types/global.d.ts
declare module "*.svg" {
  /**
   * Use `any` to avoid conflicts with
   * `@svgr/webpack` plugin or
   * `babel-plugin-inline-react-svg` plugin.
   */
  const content: any;        ^^^^^^^^^^^^
        This is the cause of the "of an `any` value" ESLint issues.

  export default content;
}

Solution

Create a custom next-env.dts to exclude image-types/global

Although Next.js owns the next-env.d.ts file, the original PR implementing this change into Next.js 11 states that we can customize tsconfig.json to add our own custom-next-env.d.ts declaration file that doesn't include the next/image-types/global module declarations.

custom-next-env.d.ts
  /// <reference types="next" />
  /// <reference types="next/types/global" />
- /// <reference types="next/image-types/global" />

We'll also need to add next-env.d.ts to .eslintignore to avoid ESLint getting confused with how to handle next-env.d.ts.

Otherwise you'll see the following error:

/home/runner/work/parent-portal/parent-portal/next-env.d.ts
  0:0  error  Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser.

The file does not match your project config: next-env.d.ts.
                                           ^^^^^^^^^^^^^
The file must be included in at least one of the projects provided

Here's an example of my .eslintignore

.eslintignore
  # Dependency directories
  /node_modules

  # NextJS Files
  /build
  /public/*
  /.next
+ next-env.d.ts

Configure tsconfig.json

We can then configure tsconfig.json to exclude the original next-env.d.ts to exclude: next-env.d.ts, and include: custom-next-env.d.ts.

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es6", "dom", "dom.iterable", "esnext"],
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "jsxImportSource": "@emotion/react",
    "baseUrl": ".",
    "paths": {
      "~/*": ["./*"],
      "~/static/*": ["./public/static/*"]
    }
  },
+ "include": ["custom-next-env.d.ts", "**/*.ts", "**/*.tsx"],+ "exclude": ["node_modules", "next-env.d.ts"]}

Create our own image-types module declarations

Now we can create a @types/images.d.ts file and take the content of the original next/image-types/global.d.ts file and specifically replace the module declaration for *.svg.

@types/images.d.ts
  type StaticImageData = {
    src: string;
    height: number;
    width: number;
    placeholder?: string;
  };

  declare module '*.png' {
    const content: StaticImageData;
    export default content;
  }

- declare module '*.svg' {
-   /**
-    * Use `any` to avoid conflicts with
-    * `@svgr/webpack` plugin or
-    * `babel-plugin-inline-react-svg` plugin.
-    */
-   const content: any
-
-   export default content
- }
+  declare module '*.svg' {+    const content: React.FC<React.SVGProps<SVGSVGElement>>;+    export default content;+  }
  declare module '*.jpg' {
    const content: StaticImageData;
    export default content;
  }

  declare module '*.jpeg' {
    const content: StaticImageData;
    export default content;
  }

  declare module '*.gif' {
    const content: StaticImageData;
    export default content;
  }

  declare module '*.webp' {
    const content: StaticImageData;
    export default content;
  }

  declare module '*.ico' {
    const content: StaticImageData;
    export default content;
  }

  declare module '*.bmp' {
    const content: StaticImageData;
    export default content;
  }

Appendix: next.config.js configuration for @svgr/webpack

For reference, here is the corresponding next.config.js to configure @svgr/webpack in the project.

$ yarn add -D @svgr/webpack
next.config.js
module.exports = {
  webpack(config) {
    config.module.rules.push({
      test: /\.svg$/i,
      // issuer section restricts svg as component only to
      // svgs imported from js / ts files.
      //
      // This allows configuring other behavior for
      // svgs imported from other file types (such as .css)
      issuer: { and: [/\.(js|ts|md)x?$/] },
      use: [
        {
          loader: "@svgr/webpack",
          options: {
            svgoConfig: { plugins: [{ removeViewBox: false }] },
          },
        },
      ],
    });
    return config;
  },
};