TypeScript Support
Blocks is built with TypeScript and provides comprehensive type definitions for all components and utilities.
Component Props Types
All components extend native HTML element types using React.ComponentProps:
import * as React from "react"
function Input ({ className , type , ... props } : React . ComponentProps < "input" >) {
return < input type = { type } className = { className } { ... props } />
}
This provides:
Full autocomplete for native HTML attributes
Type safety for event handlers
Proper typing for refs
Variant Props with CVA
Components use class-variance-authority for type-safe variants:
VariantProps automatically infers the correct types from your variant definitions.
Utility Function Types
The cn() utility function is properly typed using clsx:
import { clsx , type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn ( ... inputs : ClassValue []) {
return twMerge ( clsx ( inputs ))
}
Usage with type safety:
import { cn } from "@/lib/utils"
// All valid ClassValue types
cn ( "base-class" )
cn ( "class-1" , "class-2" )
cn ({ "conditional-class" : true })
cn ([ "array" , "of" , "classes" ])
cn ( undefined , null , "class" ) // Handles falsy values
Component Composition Types
Composite components like Card have proper type exports:
Define sub-components
import * as React from "react"
import { cn } from "@/lib/utils"
function Card ({ className , ... props } : React . ComponentProps < "div" >) {
return (
< div
className = { cn ( "rounded-xl border shadow-sm" , className ) }
{ ... props }
/>
)
}
function CardHeader ({ className , ... props } : React . ComponentProps < "div" >) {
return (
< div
className = { cn ( "grid gap-2 p-6" , className ) }
{ ... props }
/>
)
}
function CardTitle ({ className , ... props } : React . ComponentProps < "div" >) {
return (
< div
className = { cn ( "font-semibold leading-none" , className ) }
{ ... props }
/>
)
}
Export all components
export {
Card ,
CardHeader ,
CardTitle ,
CardDescription ,
CardContent ,
CardFooter ,
}
Use with full type safety
import { Card , CardHeader , CardTitle , CardContent } from "@/components/ui/card"
export default function TypedCard () {
return (
< Card >
< CardHeader >
< CardTitle > Fully Typed </ CardTitle >
</ CardHeader >
< CardContent >
All props are type-checked
</ CardContent >
</ Card >
)
}
Complex Component Types
For complex components with multiple types, define interfaces:
import type { ReactNode } from "react"
import type { SupportedLanguages } from "@pierre/diffs/react"
interface BlockViewState {
view : "preview" | "code"
size : "desktop" | "tablet" | "mobile"
}
interface BaseItem {
name : string
path : string
}
interface FileItem extends BaseItem {
type : "file"
content : string
}
interface FolderItem extends BaseItem {
type : "folder"
children : FileTreeItem []
}
type FileTreeItem = FileItem | FolderItem
interface BlocksProps {
name : string
code ?: string | ReactNode
fileTree ?: FileTreeItem []
blocksId : string
blocksCategory : string
meta ?: {
iframeHeight ?: string
type ?: "file" | "directory"
}
}
Generic Component Types
Create reusable generic components with proper typing:
import type { ReactNode } from "react"
interface ListProps < T > {
items : T []
renderItem : ( item : T , index : number ) => ReactNode
keyExtractor : ( item : T , index : number ) => string
className ?: string
}
function List < T >({
items ,
renderItem ,
keyExtractor ,
className ,
} : ListProps < T >) {
return (
< ul className = { className } >
{ items . map (( item , index ) => (
< li key = { keyExtractor ( item , index ) } >
{ renderItem ( item , index ) }
</ li >
)) }
</ ul >
)
}
// Usage with type inference
interface User {
id : string
name : string
}
const users : User [] = [
{ id: "1" , name: "Alice" },
{ id: "2" , name: "Bob" },
]
< List
items = { users }
renderItem = { ( user ) => < span > { user . name } </ span > }
keyExtractor = { ( user ) => user . id }
/>
Event Handler Types
Properly type event handlers for type safety:
Form Events
Keyboard Events
Mouse Events
import type React from "react"
import { useState } from "react"
export default function TypedForm () {
const [ value , setValue ] = useState ( "" )
const handleSubmit = ( e : React . FormEvent < HTMLFormElement >) => {
e . preventDefault ()
console . log ( value )
}
const handleChange = ( e : React . ChangeEvent < HTMLInputElement >) => {
setValue ( e . target . value )
}
return (
< form onSubmit = { handleSubmit } >
< input value = { value } onChange = { handleChange } />
</ form >
)
}
Ref Types
Properly type component refs:
import { useRef , useEffect } from "react"
import type { ImperativePanelHandle } from "react-resizable-panels"
export default function TypedRefs () {
const textareaRef = useRef < HTMLTextAreaElement >( null )
const panelRef = useRef < ImperativePanelHandle >( null )
useEffect (() => {
// TypeScript knows these might be null
if ( textareaRef . current ) {
textareaRef . current . focus ()
}
if ( panelRef . current ) {
panelRef . current . resize ( 50 )
}
}, [])
return (
<>
< textarea ref = { textareaRef } />
</>
)
}
Type Assertions and Guards
Use type guards for runtime type safety:
function isFile ( item : FileTreeItem ) : item is FileItem {
return item . type === "file"
}
function isFolder ( item : FileTreeItem ) : item is FolderItem {
return item . type === "folder"
}
// Usage
const items : FileTreeItem [] = getFileTree ()
items . forEach (( item ) => {
if ( isFile ( item )) {
// TypeScript knows item is FileItem
console . log ( item . content )
} else if ( isFolder ( item )) {
// TypeScript knows item is FolderItem
console . log ( item . children )
}
})
Extending Component Types
Extend existing component types for custom props:
import type React from "react"
import { Button , type buttonVariants } from "@/components/ui/button"
import type { VariantProps } from "class-variance-authority"
interface IconButtonProps extends React . ComponentProps < "button" >,
VariantProps < typeof buttonVariants > {
icon : React . ReactNode
label : string
asChild ?: boolean
}
export function IconButton ({
icon ,
label ,
variant ,
size = "icon" ,
... props
} : IconButtonProps ) {
return (
< Button variant = { variant } size = { size } { ... props } >
{ icon }
< span className = "sr-only" > { label } </ span >
</ Button >
)
}
Real-World Example
Here’s a complete typed component from the Blocks codebase:
"use client"
import type React from "react"
import { useRef , useState } from "react"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { cn } from "@/lib/utils"
interface ComposerState {
message : string
isExpanded : boolean
}
export default function TypedComposer () {
const [ state , setState ] = useState < ComposerState >({
message: "" ,
isExpanded: false ,
})
const textareaRef = useRef < HTMLTextAreaElement >( null )
const handleSubmit = ( e : React . FormEvent < HTMLFormElement >) => {
e . preventDefault ()
if ( state . message . trim ()) {
setState ({ message: "" , isExpanded: false })
if ( textareaRef . current ) {
textareaRef . current . style . height = "auto"
}
}
}
const handleChange = ( e : React . ChangeEvent < HTMLTextAreaElement >) => {
const newMessage = e . target . value
setState ({
message: newMessage ,
isExpanded: newMessage . length > 100 || newMessage . includes ( " \n " ),
})
if ( textareaRef . current ) {
textareaRef . current . style . height = "auto"
textareaRef . current . style . height = ` ${ textareaRef . current . scrollHeight } px`
}
}
const handleKeyDown = ( e : React . KeyboardEvent < HTMLTextAreaElement >) => {
if ( e . key === "Enter" && ! e . shiftKey ) {
e . preventDefault ()
handleSubmit ( e as unknown as React . FormEvent < HTMLFormElement >)
}
}
return (
< form onSubmit = { handleSubmit } >
< div
className = { cn (
"w-full max-w-2xl mx-auto border" ,
state . isExpanded ? "rounded-3xl" : "rounded-3xl"
) }
>
< Textarea
ref = { textareaRef }
value = { state . message }
onChange = { handleChange }
onKeyDown = { handleKeyDown }
placeholder = "Type a message"
className = "resize-none border-0"
/>
< Button type = "submit" disabled = { ! state . message . trim () } >
Send
</ Button >
</ div >
</ form >
)
}
Enable strict mode in tsconfig.json for maximum type safety.
TypeScript Configuration
The recommended tsconfig.json for Blocks:
{
"compilerOptions" : {
"target" : "ES2020" ,
"lib" : [ "ES2020" , "DOM" , "DOM.Iterable" ],
"jsx" : "preserve" ,
"module" : "ESNext" ,
"moduleResolution" : "bundler" ,
"strict" : true ,
"esModuleInterop" : true ,
"skipLibCheck" : true ,
"forceConsistentCasingInFileNames" : true ,
"resolveJsonModule" : true ,
"isolatedModules" : true ,
"incremental" : true ,
"paths" : {
"@/*" : [ "./*" ]
}
}
}
Best Practices
Use strict mode : Enable all strict type checking options
Avoid any : Use unknown or proper types instead
Export types : Export interfaces and types for component consumers
Type event handlers : Always type React event handlers explicitly
Use type inference : Let TypeScript infer types when possible
Document complex types : Add JSDoc comments for complex type definitions
Next Steps