TypeScript Union Type Too Complex: A Typing Animation Bug
While implementing a typing animation component for my portfolio website, I ran into an interesting TypeScript compilation error that had me scratching my head for a bit. The error message was obviously cryptic at first, but understanding what caused it taught me something valuable about TypeScript's type system.
The Error
Here's what showed up in my build logs:
./components/magicui/typing-animation.tsx:55:16
Type error: Expression produces a union type that is too complex to represent.
53 |
54 | return (
> 55 | <Component ref={ref} className={cn('inline-block', className)}>
| ^
56 | <motion.span
The build worked fine locally but failed when deploying to Netlify. Classic "works on my machine" situation.
The Problematic Code
I was building a reusable typing animation component that could render as any HTML element:
interface TypingAnimationProps {
children: string;
duration?: number;
delay?: number;
startOnView?: boolean;
className?: string;
as?: keyof JSX.IntrinsicElements; // This was the culprit
}
export function TypingAnimation({
children,
duration = 100,
delay = 0,
startOnView = false,
className,
as: Component = 'div',
}: TypingAnimationProps) {
const ref = useRef(null);
const isInView = useInView(ref);
// Animation logic here...
return (
<Component ref={ref} className={cn('inline-block', className)}>
{/* Content */}
</Component>
);
}
The idea was to make the component flexible—you could use it as a div
, span
, p
, or any other HTML element by passing the as
prop.
What Went Wrong?
The issue lies in how TypeScript handles the combination of dynamic component types and refs. When you use keyof JSX.IntrinsicElements
, you are telling TypeScript that this component could be ANY HTML element; that is over 100 different types!
Each HTML element has its own specific ref type:
<div>
usesHTMLDivElement
<span>
usesHTMLSpanElement
<input>
usesHTMLInputElement
- And so on...
When TypeScript tries to create a union type for all possible ref combinations with all possible HTML elements, it creates a massive type union that exceeds TypeScript's complexity limits. Hence the error: "Expression produces a union type that is too complex to represent."
The Fix
The solution was straightforward: simplify the component by removing the dynamic element type:
interface TypingAnimationProps {
children: string;
duration?: number;
delay?: number;
startOnView?: boolean;
className?: string;
// Removed the 'as' prop
}
export function TypingAnimation({
children,
duration = 100,
delay = 0,
startOnView = false,
className,
}: TypingAnimationProps) {
const ref = useRef<HTMLDivElement>(null); // Explicitly typed ref
const isInView = useInView(ref);
// Animation logic...
return (
<div ref={ref} className={cn('inline-block', className)}>
{/* Content */}
</div>
);
}
By hard-coding the element as a div
and properly typing the ref as useRef<HTMLDivElement>(null)
, the type complexity disappeared and the build succeeded.
Alternative Solutions
If you really need the flexibility of different HTML elements, here are some alternatives:
-
Limited element types: Instead of allowing any element, restrict to a few common ones:
as?: 'div' | 'span' | 'p' | 'section';
-
Separate components: Create specific variants like
TypingAnimationDiv
,TypingAnimationSpan
, etc. -
Type assertion: Use type assertions to bypass the type checking (though this reduces type safety).
Lessons Learned
This bug taught me that TypeScript's type system, while powerful, has practical limits. When creating flexible components, it's important to balance flexibility with type complexity. Sometimes, a simpler solution that covers 95% of use cases is better than a complex one that tries to handle everything.
In my case, the typing animation component really only needed to be a div
anyway, so the simpler solution was actually the better one.