🔧 Storybook + TypeScript + Babel: Setup Guide
Questions Answered by This Post
- How do I set up Storybook to work with TypeScript files?
- What's the high-level approach to configuring Babel with Storybook?
- Why does webpack fail when processing TypeScript files directly?
- What's the minimal configuration needed to get Storybook + TypeScript working?
- Why should I use a unified Babel configuration?
- What value does this setup provide?
This guide provides a practical setup for configuring Storybook with TypeScript and Babel. You'll learn the high-level approach and why this configuration matters for your development workflow.
Why This Setup Matters
When working with Storybook and TypeScript, you need a way to convert TypeScript to JavaScript before webpack processes it. Here's why this setup is valuable:
1. Unified Configuration
- One
babel.config.jsfor the entire project - Works for both Docusaurus and Storybook
- Easier to maintain and understand
2. No Pre-compilation Needed
- babel-loader handles transpilation on-the-fly
- No separate build step required
- Faster development workflow
3. Clean Separation of Concerns
- TypeScript handles type checking (in your IDE)
- Babel handles transpilation (at build time)
- Webpack handles bundling
- Each tool does what it's best at
4. Better Developer Experience
- Type safety during development
- Fast build times
- Clear error messages
- Works seamlessly with modern tooling
The Approach: A Unified Babel Configuration
The key insight is that Babel handles TypeScript transpilation, and we need to ensure babel-loader runs before webpack tries to parse the files. Here's the high-level approach:
- Babel transpiles TypeScript to JavaScript - Babel removes type annotations and converts TypeScript syntax to JavaScript
- babel-loader runs first - We configure webpack to process TypeScript files through babel-loader before other loaders
- Unified configuration - One
babel.config.jsshared between Docusaurus and Storybook
The Setup
1. Babel Configuration (babel.config.js)
The main Babel configuration lives at the project root and is shared between Docusaurus and Storybook:
module.exports = {
presets: [
[require.resolve('@babel/preset-typescript'), {
isTSX: true,
allExtensions: true
}],
require.resolve('@docusaurus/core/lib/babel/preset'),
],
};
What this does:
@babel/preset-typescript- Strips TypeScript syntax (includingimport type) and converts it to JavaScript@docusaurus/core/lib/babel/preset- Docusaurus-specific transformations
2. Storybook Configuration (.storybook/main.ts)
Storybook needs to be told to use babel-loader for TypeScript files:
webpackFinal: async (config, { configDir }) => {
// ... other config ...
// Ensure babel-loader processes TypeScript files FIRST
if (config.module?.rules) {
config.module.rules.unshift({
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('babel-loader'),
// babel-loader automatically uses babel.config.js from project root
},
],
});
}
return config;
}
Key points:
unshift()adds the rule at the beginning of the rules array- This ensures babel-loader runs before Storybook's CSF plugin
- babel-loader automatically finds and uses
babel.config.js
3. TypeScript Configuration (tsconfig.json)
We consolidated to a single tsconfig.json that works for both Docusaurus and Storybook:
{
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"isolatedModules": true
},
"include": [
"src/**/*",
".storybook/**/*"
]
}
The Complete Setup
Here's what we ended up with:
File Structure
bytesofpurpose-blog/
├── babel.config.js # Shared Babel config (Docusaurus + Storybook)
├── tsconfig.json # Shared TypeScript config
├── .storybook/
│ ├── main.ts # Storybook webpack config
│ └── preview.ts # Storybook preview config
└── src/
└── **/*.stories.tsx # Story files
Configuration Files
babel.config.js - The single source of truth for transpilation:
module.exports = {
presets: [
[require.resolve('@babel/preset-typescript'), {
isTSX: true,
allExtensions: true
}],
require.resolve('@docusaurus/core/lib/babel/preset'),
],
};
.storybook/main.ts - Minimal webpack configuration:
webpackFinal: async (config) => {
// Add babel-loader rule FIRST
config.module.rules.unshift({
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: [{ loader: require.resolve('babel-loader') }],
});
return config;
}
Key Takeaways
- Babel transpiles, TypeScript types - TypeScript provides types, Babel removes them
- Webpack needs JavaScript - It can't parse TypeScript directly
- Loader order matters - babel-loader must run before other loaders
- Unified config is better - One
babel.config.jsfor the whole project - No pre-compilation needed - babel-loader handles it on-the-fly
What's Next?
- For understanding the tools and their responsibilities, see Understanding the Tools
- For the development process and build flow, see Development Process & Experience
- For common pitfalls and debugging tips, see Common Pitfalls & Issues