How to Use class instead of className with Preact and TypeScript
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;
}
}
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>
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
toclassName
- 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>
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
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;
}
}
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: zeit/next.js#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;
}
}
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.