Switching to TypeScript can be tough, whether you're learning the language or updating legacy JavaScript projects. Manually converting large amounts of code is time-consuming and prone to errors. Enter TypeSavior—an AI-powered tool designed to simplify this migration process.
TypeSavior streamlines the transition by explaining each change. Simply select an OpenAI model, paste your JavaScript code, and convert it to TypeScript. Then, review the summary to understand the changes and improvements in type safety.
How Does It Work?
TypeSavior leverages OpenAI's language models to automatically convert JavaScript code to TypeScript. Beyond that, it also provides detailed summaries of the changes, helping developers understand type safety improvements or learn TypeScript more effectively.
Live Project
https://typesavior.vercel.app/
Features
- Code Conversion: Paste your JavaScript code and receive the TypeScript version.
- Detailed Summaries: Get concise explanations of the changes made, helping you learn TypeScript.
- Model Selection: Choose between different OpenAI models for code conversion.
- Copy to Clipboard: Easily copy the converted TypeScript code for use in your projects.
- User-Friendly Interface: Simple and intuitive design for ease of use.
Getting Started
First, clone the repository and install the dependencies:
git clone https://github.com/JPerez00/typesavior
cd typesavior
npm install
Or
npx create-next-app --example https://github.com/JPerez00/typesavior [Your-Project-Name-Here]
cd [Your-Project-Name-Here]
Setting Up Environment Variables
To run the project locally, create a .env.local
file in the root directory and add your OpenAI API key as shown below:
OPENAI_API_KEY=your-api-key-here
When deploying to Vercel, ensure you configure the same environment variables in your project's settings under the Environment Variables
section.
key=OPENAI_API_KEY
value=your-api-key-here
Running the Server
Start the development server:
npm run dev
Open http://localhost:3000 with your browser to see the application.
Project Info, Dependencies & Packages Used
This is a Next.js 14 app that uses TypeScript, Tailwind CSS & the App Router. Here are the exact npm packages I installed.
npm install @codemirror/lang-javascript @codemirror/theme-one-dark @headlessui/react @heroicons/react @tailwindcss/typography @uiw/react-codemirror clsx lucide-react openai react-markdown
Project Directory
First, let's take a look at the project Directory.
typesavior/
├── public/
│ ├── ts-hero-banner.png
│ ├── (additional imgs, svgs etc)
├── src/
│ ├── app/
│ │ ├── api/
│ │ │ └── convert/
│ │ │ └── route.ts
│ │ ├── components/
│ │ │ ├── ModelDropDown.tsx
│ │ │ └── ... (other reusable components)
│ │ ├── page.tsx
│ │ └── layout.tsx
│ ├── styles/
│ │ ├── globals.css
│ │ └── ... (additional style files)
├── .env.local
├── .gitignore
├── next.config.js
├── package.json
├── tsconfig.json
├── README.md
└── yarn.lock / package-lock.json
1. The API Route
The heart of TypeSavior lies in its ability to communicate with OpenAI's API to perform code conversions. The route.ts
file under the api/convert/
directory handles this functionality, things like:
- Handling POST Requests: The API route listens for POST requests containing JavaScript code and the selected OpenAI model.
- Interacting with OpenAI's API: It sends the provided JavaScript code to OpenAI for conversion to TypeScript.
- Processing the Response: Upon receiving the converted code and summary from OpenAI, it parses and returns this data to the client.
// src/app/api/convert/route.ts
import { NextResponse } from 'next/server';
import OpenAI from 'openai';
export const runtime = 'nodejs';
export async function POST(request: Request) {
try {
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const { code, model } = await request.json();
if (!code) {
console.error('No code provided.');
return NextResponse.json({ error: 'No code provided.' }, { status: 400 });
}
const validModels = ['gpt-3.5-turbo', 'gpt-4', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'];
const selectedModel = validModels.includes(model) ? model : 'gpt-3.5-turbo';
const prompt = `
Convert the following JavaScript code to TypeScript.
Provide the TypeScript code first, then a concise summary explaining the changes and types used, tailored for a JavaScript developer learning TypeScript.
**Please format your summary using Markdown, with clear paragraphs and bullet points where appropriate.**
Please format your response exactly as follows, and do not include any additional text or explanations outside of these tags:
<TypeScriptCode>
[TypeScript code here]
</TypeScriptCode>
<Summary>
[Summary here]
</Summary>
JavaScript Code:
${code}
`;
const completion = await openai.chat.completions.create({
model: selectedModel,
messages: [{ role: 'user', content: prompt }],
temperature: 0,
});
let responseText = completion.choices[0].message?.content || '';
// Remove any text before the first <TypeScriptCode> tag
const startIndex = responseText.indexOf('<TypeScriptCode>');
if (startIndex !== -1) {
responseText = responseText.slice(startIndex);
} else {
console.error('TypeScriptCode tag not found in response.');
return NextResponse.json(
{
error: 'Failed to find <TypeScriptCode> tag in OpenAI response.',
},
{ status: 500 }
);
}
// Extract TypeScript code and summary using regex
const tsCodeMatch = responseText.match(
/<TypeScriptCode>\s*([\s\S]*?)\s*<\/TypeScriptCode>/i
);
const summaryMatch = responseText.match(
/<Summary>\s*([\s\S]*?)\s*<\/Summary>/i
);
if (!tsCodeMatch) {
console.error('Failed to extract TypeScript code.');
return NextResponse.json(
{
error: 'Failed to extract TypeScript code from OpenAI response.',
},
{ status: 500 }
);
}
if (!summaryMatch) {
console.error('Failed to extract summary.');
return NextResponse.json(
{
error: 'Failed to extract summary from OpenAI response.',
},
{ status: 500 }
);
}
const tsCode = tsCodeMatch[1].trim();
const summary = summaryMatch[1].trim();
return NextResponse.json({ tsCode, summary });
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Error in API route:', error.message);
return NextResponse.json(
{ error: 'Internal server error', details: error.message },
{ status: 500 }
);
} else {
console.error('Unknown error:', error);
return NextResponse.json(
{ error: 'Internal server error', details: 'An unexpected error occurred.' },
{ status: 500 }
);
}
}
}
Behind the Design:
-
Runtime Specification: By exporting runtime = 'nodejs', we ensure that the API route runs in a Node.js environment, which is necessary for server-side operations like API calls.
-
Model Validation: The route validates the selected OpenAI model against a list of supported models to prevent invalid requests.
-
Prompt Structuring: The prompt is carefully crafted to instruct OpenAI to return the TypeScript code and a markdown-formatted summary within specific tags. This structure simplifies parsing the response.
-
Error Handling: Comprehensive error handling ensures that both client-side and server-side issues are gracefully managed, providing meaningful feedback to users.
2. The Model Selection Dropdown
This component lets you select the desired OpenAI model. It’s great for providing flexibility and options, just in case.
The ModelDropDown component provides an intuitive interface for this selection, which handles:
- Displaying Available Models: It lists the supported OpenAI models for selection.
- Handling User Selection: Updates the selected model state based on user interaction.
import { Fragment } from 'react';
import { Menu, Transition } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/20/solid';
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ');
}
interface ModelDropDownProps {
selectedModel: string;
setSelectedModel: (model: string) => void;
}
export const ModelDropDown: React.FC<ModelDropDownProps> = ({
selectedModel,
setSelectedModel,
}) => {
const models = ['gpt-3.5-turbo', 'gpt-4', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini'];
return (
<Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="inline-flex items-center gap-1 rounded-lg bg-slate-800 py-2 px-3 text-sm/6 font-semibold text-white shadow-inner shadow-white/20 focus:outline-none data-[hover]:bg-slate-700 data-[open]:bg-slate-700 data-[focus]:outline-1 data-[focus]:outline-white">
<span>{selectedModel.toUpperCase()}</span>
<ChevronDownIcon className="w-5 h-5 -mr-1" aria-hidden="true" />
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute mt-2 w-56 rounded-lg shadow-lg bg-slate-800 ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-0">
{models.map((model) => (
<Menu.Item key={model}>
{({ active }) => (
<button
onClick={()=> setSelectedModel(model)}
className={classNames(
active ? 'bg-slate-700 text-white font-bold rounded' : 'text-zinc-400',
'block w-full text-left px-4 py-2 text-sm'
)}
>
{model.toUpperCase()}
</button>
)}
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
);
};
Behind the Design:
-
Headless UI Integration: Utilizing @headlessui/react and @heroicons/react provides accessible and customizable UI components without imposing specific styles, allowing for seamless integration with Tailwind CSS.
-
Dynamic Class Management: The classNames function aids in conditionally applying classes based on the component's state, enhancing readability and maintainability.
-
State Management: By receiving selectedModel and setSelectedModel as props, the component remains reusable and decoupled from the state management logic of its parent component.
-
Accessibility: Proper use of ARIA attributes and keyboard navigability ensures that the dropdown is accessible to all users.
3. The Main Interface
The page.tsx file
integrates the code editors, model selector, and summary display to provide a cohesive user experience, It also handles:
- State Management: Manages the state for JavaScript code input, TypeScript output, summary, loading status, and model selection.
- Handling Conversions: Initiates the conversion process by communicating with the API route and updating the UI based on the response.
- User Interactions: Allows users to select models, input code, trigger conversions, and copy results to the clipboard.
'use client';
import { AnchorHTMLAttributes, ClassAttributes, useState } from 'react';
import { ModelDropDown } from '../components/ModelDropDown';
import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import ReactMarkdown from 'react-markdown';
import { Copy, Check } from 'lucide-react';
import Image from 'next/image';
function Pin(
props: JSX.IntrinsicAttributes & ClassAttributes<HTMLAnchorElement> & AnchorHTMLAttributes<HTMLAnchorElement>
) {
return (
<a
{...props}
target="_blank"
className="mr-0.5 ml-0.5 inline-flex items-center rounded-md border p-1 text-sm leading-4 no-underline border-white/30 bg-zinc-800 text-zinc-200 shadow"
/>
);
}
export default function Home() {
const [jsCode, setJsCode] = useState('// Write or paste your JavaScript code here');
const [tsCode, setTsCode] = useState('// Get your TypeScript code here');
const [summary, setSummary] = useState('');
const [loading, setLoading] = useState(false);
const [selectedModel, setSelectedModel] = useState('gpt-3.5-turbo');
const [conversionComplete, setConversionComplete] = useState(false);
const [copied, setCopied] = useState(false);
const handleConvert = async () => {
setLoading(true);
setConversionComplete(false);
try {
const response = await fetch('/api/convert', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code: jsCode, model: selectedModel }),
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
setTsCode(data.tsCode);
setSummary(data.summary);
setConversionComplete(true);
} catch (error) {
console.error(error);
setSummary('An error occurred during conversion.');
}
setLoading(false);
};
const copyToClipboard = () => {
navigator.clipboard.writeText(tsCode).then(
() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
},
(err) => {
console.error('Failed to copy text: ', err);
}
);
};
return (
<div className="w-full flex flex-col items-center mb-20 bg-gradient-to-b from-sky-950 from-10% via-teal-900 via-35% to-zinc-950 to-80% rounded-t-3xl">
<div className="container mx-auto px-2 py-10">
{/* Header Section */}
<div className="text-center mb-6">
<h1 className="mb-4 text-5xl md:text-7xl leading-tight md:leading-tight tracking-tighter font-extrabold bg-gradient-to-br from-white from-40% to-gray-400 to-80% bg-clip-text text-transparent">
Say Goodbye To Your Untyped Mess
</h1>
<p className="text-balance md:text-xl text-zinc-300">
Switching to{' '}
<Pin href="https://www.typescriptlang.org/">
<Image
alt="TS logomark"
src="/ts-logo.svg"
className="!mr-1"
width="12"
height="12"
/>
TypeScript
</Pin>{' '}
can be tough. This tool simplifies the process by explaining each change. Select an{' '}
<Pin href="https://openai.com/">
<Image
alt="OpenAI logomark"
src="/openai-logomark.svg"
className="!mr-1"
width="12"
height="12"
/>
Open AI
</Pin>{' '}
model, paste your JavaScript, and convert it to TypeScript. Review the summary for
changes and type safety improvements.
</p>
</div>
{/* Model Selector */}
<div className="flex justify-center mb-6 gap-x-2 relative z-50">
<label className="text-zinc-300 text-center font-semibold text-lg mt-1.5">
Select Your Model:
</label>
<ModelDropDown selectedModel={selectedModel} setSelectedModel={setSelectedModel} />
</div>
{/* Editors */}
<div className="flex flex-col md:flex-row gap-12 md:gap-6">
{/* JavaScript Editor */}
<div
className="flex-1 min-w-0 bg-zinc-900 rounded-lg border border-white/15 shadow-lg p-2"
style={{ height: '520px' }}
>
<div className="flex space-x-2 mb-2">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<div className="w-3 h-3 rounded-full bg-green-500"></div>
</div>
<h2 className="font-bold mb-2 text-lg text-zinc-300">JavaScript Terminal</h2>
<div className="overflow-auto h-full">
<CodeMirror
value={jsCode}
height="440px"
extensions={[javascript({ typescript: false })]}
theme={oneDark}
onChange={(value)=> setJsCode(value)}
editable={!loading}
className="border border-white/15"
/>
</div>
</div>
{/* TypeScript Editor */}
<div
className="flex-1 min-w-0 bg-zinc-900 rounded-lg border border-white/15 shadow-lg p-2"
style={{ height: '520px' }}
>
<div className="flex space-x-2 mb-2">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<div className="w-3 h-3 rounded-full bg-green-500"></div>
</div>
<h2 className="font-semibold mb-2 text-lg text-zinc-300">TypeScript Terminal</h2>
{loading ? (
<div className="text-white p-2 h-full overflow-auto flex">
<svg
className="animate-spin mt-1.5 mr-3 h-4 w-4 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<p className="text-lg font-semibold animate-pulse">Converting...</p>
</div>
) : (
<div className="relative">
<div className="overflow-auto h-full">
<CodeMirror
value={tsCode}
height="440px"
extensions={[javascript({ typescript: true })]}
theme={oneDark}
readOnly
className="border border-white/15"
/>
</div>
<button
onClick={copyToClipboard}
className="absolute top-1 right-2 text-zinc-300 hover:text-white bg-gray-700 p-1 shadow-md rounded-md ring-1 ring-white/20 transition-colors z-10"
aria-label="Copy to clipboard"
>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</button>
</div>
)}
</div>
</div>
{/* Convert Button */}
<div className="text-center my-10">
<button
className="inline-flex items-center rounded-lg bg-slate-800 py-4 px-8 text-lg font-semibold text-white shadow-inner shadow-white/20 focus:outline-none hover:bg-slate-700"
onClick={handleConvert}
disabled={loading}
>
{loading && (
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" ="10" stroke="currentColor" stroke-width="4"></circle>
<path className="opacity-75" fill="currentColor" ="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{loading ? 'Converting...' : 'Convert to TypeScript'}
</button>
</div>
{/* Summary */}
{conversionComplete && (
<div className="mt-10 mb-20 max-w-5xl mx-auto w-full py-2 px-4 bg-zinc-900 rounded-lg border border-white/10 shadow-md p-2">
<div className="flex space-x-2 mb-4">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<div className="w-3 h-3 rounded-full bg-green-500"></div>
</div>
<h2 className="font-bold text-xl tracking-tight mb-2">Reasoning Behind The Conversion & Summary:</h2>
<div className="max-w-5xl mx-auto w-full px-2.5 md:px-6 py-4 bg-gray-700/40 text-white prose prose-invert border border-white/5">
<ReactMarkdown>{summary}</ReactMarkdown>
</div>
</div>
)}
</div>
</div>
);
}
Behind the Design:
Reasoning Behind the Implementation:
-
State Management: Utilizing React's useState hook, the component manages multiple states, including the input JavaScript code, the resulting TypeScript code, conversion summaries, loading indicators, model selection, and copy-to-clipboard feedback.
-
API Interaction: The handleConvert function orchestrates the communication with the API route. It sends the JavaScript code and selected model, handles the response, and updates the UI accordingly.
-
User Experience Enhancements:
- Loading Indicators: Visual feedback during the conversion process keeps users informed about ongoing operations.
- Copy to Clipboard: A convenient button allows users to quickly copy the converted TypeScript code.
- Responsive Design: Flexbox layouts and responsive classes ensure that the interface adapts gracefully to different screen sizes.
-
Accessibility: ARIA labels and semantic HTML elements enhance the application's accessibility, making it usable for a broader audience.
-
Component Reusability: The ModelDropDown component is decoupled and reusable, promoting cleaner code and easier maintenance.
Conclusion
TypeSavior is just another example of tools that make developers’ lives easier. By exploring key files like route.ts
, ModelDropDown.tsx
, and page.tsx
, we’ve broken down the logic that powers the app.
Whether you’re migrating projects to TypeScript, building your own tools, or tackling TypeScript for the first time as a beginner, these takeaways can help you get started.
Hope you guys find it interesting or useful, Cheers!