How to Use class instead of className with Preact and TypeScript

swyx - Apr 5 '20 - - Dev Community

Bottom Line Up Front

If you are using TypeScript with Preact aliased as React, you can add an ambient declaration to use class instead of className:

// anyname.d.ts - place this anywhere in your project
declare namespace React {
  interface HTMLAttributes<T> {
    // Preact supports using "class" instead of "classname" - need to teach typescript
    class?: string;
  }
}
Enter fullscreen mode Exit fullscreen mode

My Context

Most React users aren't using every feature of React. For most usecases, using Preact is exactly equivalent. As point of proof: This blogpost you are reading is written in a custom CMS in Preact, by me, a React dev who has never used Preact and only skimmed the Differences page on their docs.

This is a good idea because of the limited downside - you can swap back to React in a single command if you end up needing its power - and the instant upside - A Next.js + Preact page bundle will now come in as low as 20kb of JS whereas a trip to the Create Next App docs will download at least 100kb of JS off the bat (note: these aren't apples to apples comparisons). If you'd like to give it a try, I've been building a small Preact + Next.js + TypeScript + TailwindCSS starter repo.

However I'm not here to write about anything as important as all that 😂. I'm here writing about a much smaller benefit of using Preact - you can use class instead of className!

Why class over className

This is mainly a pain point because I use Tailwind UI, which involves a lot of copying and pasting mountains of code that looks like this:

<div class="relative bg-gray-50 overflow-hidden">
  <div class="hidden sm:block sm:absolute sm:inset-y-0 sm:h-full sm:w-full">
    <div class="relative h-full max-w-screen-xl mx-auto">
      <svg class="absolute right-full transform translate-y-1/4 translate-x-1/4 lg:translate-x-1/2" width="404" height="784" fill="none" viewBox="0 0 404 784">
        <defs>
          <pattern id="f210dbf6-a58d-4871-961e-36d5016a0f49" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
            <rect x="0" y="0" width="4" height="4" class="text-gray-200" fill="currentColor" />
          </pattern>
        </defs>
        <rect width="404" height="784" fill="url(#f210dbf6-a58d-4871-961e-36d5016a0f49)" />
      </svg>
      <svg class="absolute left-full transform -translate-y-3/4 -translate-x-1/4 md:-translate-y-1/2 lg:-translate-x-1/2" width="404" height="784" fill="none" viewBox="0 0 404 784">
        <defs>
          <pattern id="5d0dd344-b041-4d26-bec4-8d33ea57ec9b" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
            <rect x="0" y="0" width="4" height="4" class="text-gray-200" fill="currentColor" />
          </pattern>
        </defs>
        <rect width="404" height="784" fill="url(#5d0dd344-b041-4d26-bec4-8d33ea57ec9b)" />
      </svg>
    </div>
  </div>
Enter fullscreen mode Exit fullscreen mode

The Problem

Now because I'm using Next.js and TypeScript with Preact, I use Preact with a React alias - basically lying to TypeScript that we are using React so we benefit from it's mature tooling across VS Code and Next.js.

However React doesn't use class for classes, it uses className! (At least until React Fire lands.) So I have two choices:

  • either go through and rename every class to className - like a heathen - every time I use Tailwind
  • or try to use class as Preact lets me do

The problem goes back to what I stated above: we lied to TypeScript that we're using React, so it's not going to let us use class:

This:

<div class="bg-black">Does this work?</div>
Enter fullscreen mode Exit fullscreen mode

Leads to:

(JSX attribute) class: string
Type '{ children: string; href: string; class: string; }' is not assignable to type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'.
  Property 'class' does not exist on type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'.ts(2322)
Peek Problem
No quick fixes available
Enter fullscreen mode Exit fullscreen mode

Oh no! No quick fixes available??

Lies.

The Quick Fix

Here's the fix. Add a TypeScript ambient declaration - basically a anyname.d.ts file anywhere in your project, assuming default tsconfig settings and add this:

// anyname.d.ts - place this anywhere in your project
declare namespace React {
  interface HTMLAttributes<T> {
    // Preact supports using "class" instead of "classname" - need to teach typescript
    class?: string;
  }
}
Enter fullscreen mode Exit fullscreen mode

And now I can write class in my React code!

If that's all you came to this blogpost for, then we're done. I'm just going to discuss how I made my way to this solution as an intermediate TypeScript user, since this is a learning opportunity for broader TypeScript use.

⚠️ If you're also using Tailwind with Next.js, there is one more issue to resolve - disabling the styled-jsx plugin. More details here: https://github.com/zeit/next.js/issues/11675

Appendix: Declaration Merging to Patch Definitions

One thing I solidified when I read Boris Cherny's TypeScript book is the power of Declaration Merging to fix issues with official typings. I even included this as part of the Troubleshooting Handbook in the React TypeScript Cheatsheet.

But I'd only patched libraries before, never patched something in the core behavior of JSX itself.

When I googled how to do this, I found this unhelpful Stackoverflow answer and eventually this Stackoverflow answer:

declare module 'react' {
     interface HTMLProps<T> {
        block?:string;
        element?:string;
        modifiers?:string;
    }
}
Enter fullscreen mode Exit fullscreen mode

But when I tried it, it didn't work.

I ultimately resorted to looking up the React typings in DefinitelyTyped itself, and realizing that React was exported as a namespace. Since Namespaces are a more arcane feature of TypeScript, I've never really used them in application code.

But it was enough to go on - I swapped declare module 'react' with declare namespace React and prayed that declaration merging worked.

It did. And now you know too.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .