terministic steps: macro detection, type extraction, interface generation, and signature injection.
Step 1: Macro Detection & AST Traversal
The compiler scans <script setup> blocks for defineProps() calls. It distinguishes between three declaration forms:
- Type-argument form:
defineProps<{ ... }>()
- Array form:
defineProps(['key1', 'key2'])
- Object form:
defineProps({ key1: { type: String, required: true } })
The type-argument form is prioritized because it contains explicit TypeScript nodes. The compiler extracts the TSTypeLiteral node and preserves its structure.
For the type-argument form, the compiler isolates the generic type parameter. It maps Vue's optional syntax (?) and required syntax directly to TypeScript's property modifiers. Runtime object declarations are parsed to extract type, required, and default fields, then converted to equivalent TypeScript types.
Step 3: Interface Generation
The extracted type nodes are transformed into a standalone TypeScript interface. The compiler prefixes the interface name with I followed by the component name to avoid naming collisions. All properties are marked as readonly to enforce React's immutability contract.
Step 4: Component Signature Injection
The generated interface replaces the Vue macro. The compiler removes the defineProps() call entirely and injects the interface into the React function component's parameter list. Destructuring patterns are preserved or normalized based on linter configuration.
New Code Examples
Vue 3 Source
<script setup lang="ts">
interface ActionPayload {
actionType: 'submit' | 'cancel';
metadata?: Record<string, unknown>;
}
const emit = defineEmits<{
(e: 'execute', payload: ActionPayload): void;
}>();
const widgetProps = defineProps<{
tenantId: string;
themeVariant: 'light' | 'dark' | 'system';
isLoading: boolean;
onSubmit?: (data: ActionPayload) => void;
}>();
</script>
Compiled React Output
import type { FC } from 'react';
interface IWidgetProps {
readonly tenantId: string;
readonly themeVariant: 'light' | 'dark' | 'system';
readonly isLoading: boolean;
readonly onSubmit?: (data: { actionType: 'submit' | 'cancel'; metadata?: Record<string, unknown> }) => void;
}
const Widget: FC<IWidgetProps> = ({ tenantId, themeVariant, isLoading, onSubmit }) => {
// Component logic
return null;
};
export default Widget;
Architecture Decisions & Rationale
Why static interfaces over runtime validation?
React's reconciliation engine expects props to be plain objects. Introducing runtime validation hooks forces additional function calls during render cycles, breaking React's pure component assumptions. TypeScript's compile-time checking provides identical safety guarantees with zero runtime cost.
Why explicit interface declaration instead of inline typing?
Extracting props into a named interface (IWidgetProps) improves IDE navigation, enables interface extension for testing mocks, and aligns with React's convention of separating type definitions from component logic. It also simplifies prop drilling analysis in large codebases.
Why preserve destructuring patterns?
Vue developers frequently destructure props immediately. React linters (ESLint react/destructuring-assignment) accept both props.x and destructured patterns. The compiler maintains the original destructuring to reduce cognitive load during migration, while allowing teams to enforce consistent patterns via post-compilation lint rules.
Pitfall Guide
Cross-framework prop translation introduces subtle edge cases. Ignoring them leads to type drift, runtime crashes, or degraded developer experience.
1. The Runtime Validation Mirage
Explanation: Developers assume Vue's object-form defineProps({ type: String, required: true }) must be converted to React PropTypes or a runtime validation hook.
Fix: Convert runtime declarations to TypeScript interfaces during compilation. If runtime validation is required, integrate Zod or Valibot at the component boundary, not inside the prop translation layer.
2. Optional vs Required Ambiguity
Explanation: Vue's ? syntax and React's optional parameters behave identically in TypeScript, but migration tools sometimes strip optionality when parsing array forms like defineProps(['a', 'b']).
Fix: Configure the AST parser to default array-form properties to unknown or any with explicit ? modifiers. Enforce strict type annotation before compilation to prevent silent type widening.
3. Generic Prop Leakage
Explanation: Vue supports defineProps<T>() where T extends a base interface. Naive compilers generate interface ICompProps extends T, which breaks React's generic component signature requirements.
Fix: Generate generic function components: function Comp<T extends BaseProps>(props: T). Preserve the generic constraint in the interface definition and pass it through the component signature.
4. Default Value Stripping
Explanation: Vue's withDefaults(defineProps<{...}>(), { key: 'value' }) is frequently ignored during translation, leaving React components without fallback values.
Fix: Map withDefaults to React's default parameter syntax: ({ key = 'value' }: ICompProps). Alternatively, generate inline fallbacks inside the component body if destructuring is avoided.
5. Over-Inference to any
Explanation: Array-form declarations (defineProps(['foo', 'bar'])) lose type information. Compilers that blindly map these to any break strict TypeScript configurations.
Fix: Flag array-form props as unknown and require explicit type narrowing. Provide a migration lint rule that forces developers to convert array forms to type-argument forms before compilation.
6. Event Handler Signature Mismatch
Explanation: Vue's defineEmits and React's callback props use different naming conventions and payload structures. Translating emit('update', val) to props.onUpdate(val) without signature alignment causes type errors.
Fix: Generate callback props with explicit payload types. Map Vue's kebab-case events to camelCase React props automatically, and enforce consistent payload interfaces across the translation pipeline.
7. Destructuring Order Dependency
Explanation: Vue allows arbitrary destructuring order. React components sometimes rely on prop order for performance optimizations (e.g., React.memo shallow comparison).
Fix: Normalize destructuring to alphabetical or declaration order during compilation. Document the convention in the migration guide to prevent subtle memoization bugs.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Strict type safety required | Static AST Translation | Preserves exact TypeScript interfaces, eliminates runtime validation, aligns with React conventions | Low (compiler setup) |
| Legacy Vue runtime props dominate | Hybrid Translation + Zod | Converts static types to interfaces, wraps runtime validation in lightweight schema libraries | Medium (schema maintenance) |
| Rapid prototyping / MVP | Runtime Adapter Hook | Fastest migration path, minimal compiler configuration, acceptable for non-production environments | High (performance debt) |
| Performance-critical applications | Static AST Translation + Manual Review | Zero runtime overhead, full type fidelity, allows fine-tuning of memoization and re-render boundaries | Low-Medium (review time) |
Configuration Template
// vue-to-react.config.ts
import { defineConfig } from '@codcompass/vue-react-compiler';
export default defineConfig({
input: './src/vue-components/**/*.vue',
output: './src/react-components',
typescript: {
strict: true,
interfacePrefix: 'I',
preserveDestructuring: true,
defaultToUnknownForArrays: true,
},
runtime: {
validation: 'none', // 'none' | 'zod' | 'valibot'
emitToCallback: true,
kebabToCamel: true,
},
transforms: {
withDefaults: 'reactDefaultParams',
genericProps: 'preserveConstraints',
optionalHandling: 'exactOptional',
},
lint: {
enforcePropAccess: 'destructuring',
maxPropCount: 8,
warnOnAnyInference: true,
},
});
Quick Start Guide
- Install the compiler package: Run
npm install @codcompass/vue-react-compiler typescript --save-dev to set up the translation pipeline.
- Create the configuration file: Copy the Configuration Template above into
vue-to-react.config.ts at your project root. Adjust input/output paths to match your directory structure.
- Run the translation: Execute
npx vue-react-compiler compile to process your Vue SFCs. The compiler will generate TypeScript interfaces and React function components in the output directory.
- Verify type safety: Run
npx tsc --noEmit against the generated output. Fix any flagged type mismatches, then integrate the components into your React application.
- Validate runtime behavior: Mount the translated components in a test environment. Confirm that prop passing, event callbacks, and default values behave identically to the original Vue components.