DEV Community

Cover image for Meet type-tune! 🦀 Give Your TypeScript Types a Better Home! 😸
nyaomaru for Necoz

Posted on AI-assisted

Meet type-tune! 🦀 Give Your TypeScript Types a Better Home! 😸

Addresses type clutter from AI-generated code

Hoi hoi! 👋

I'm @nyaomaru, a frontend engineer who is actually allergic to crabs! 🙀🦀

Today, I have another exciting announcement!

I've just released a new open-source tool called type-tune! 🦀🎉

GitHub logo necoz-oss / type-tune

A Rust-powered CLI to find shared TypeScript types and move them safely. 🦀

type-tune

Animated type-tune logo

Test Docs MIT License

Give shared TypeScript types a better home. 😸

Scan. Review. Move. Validate.

type-tune is a Rust-powered CLI that finds shared TypeScript type aliases and interface declarations and helps move them without manually chasing imports.

It creates a reviewable plan, updates supported type imports and re-exports, and validates the rewrite with TypeScript. You decide what moves and where.

Guides for installation, the first scan, and the review workflow live in the official documentation. Releases are listed on GitHub Releases, and the package is type-tune on npm.

Why type-tune?

Types often start next to a component or utility, then end up shared across a project. Moving them later means tracking imports, local dependencies, and barrels by hand.

  • Discover shared types with deterministic candidate IDs.
  • Review plan.md and choose the destination before making changes.
  • Rewrite supported type-only imports, aliases, and simple barrels while keeping compatibility re-exports at the original…

It's a small CLI tool built with Rust 🦀 that helps you find shared TypeScript types and move them to a better place.

Scan. Review. Move. Validate.

Sounds simple, right?

Well, that's exactly the idea! 😸


🐈 What Is type-tune?

If you've worked on a large TypeScript project, you've probably used tools like Knip.

Knip is awesome! 🎉

It helps us find unused files, exports, and dependencies, making it easier to clean up our codebases.

But there's another problem I've been thinking about.

What about types that are still being used, but aren't organized very well?

TypeScript types are incredibly reusable.

A type declared in one component might eventually be needed by another component, an API module, or even an entire feature.

But in practice, we don't always pay much attention to maintaining our type declarations.

For example, imagine we have this

// src/features/order/OrderPage.tsx
export type OrderSummary = {
  id: string;
  total: number;
};
Enter fullscreen mode Exit fullscreen mode

At first, OrderSummary might only be used by OrderPage.

But as the feature grows, other files start importing it.

And sometimes, developers end up declaring similar types again because they don't realize something reusable already exists.

Eventually, we might have:

  • Types scattered across components and utilities.
  • Similar type declarations repeated within the same feature.
  • Shared types living in files that don't really belong to them.
  • Imports that make refactoring unnecessarily complicated.

Everything still works!

But is it easy to maintain?

Hmm, not really. 😹

That's where type-tune comes in!

type-tune finds exported types and interfaces that are already shared across multiple files and helps relocate them to more appropriate locations.

It can also update supported imports and preserve compatibility re-exports, so we don't have to manually chase references around the project.

type-tune workflow

Unlike Knip, which focuses on unused code, type-tune focuses on organizing types that are actually being used.

They're complementary tools!

One helps us remove what we don't need. The other helps us organize what we still need. 😸


💡 Why Did I Build It?

This idea actually came from my own experience.

I was working on refactoring a production codebase with more than 300,000 lines of code.

And one thing kept bothering me.

We weren't making very good use of our existing types.

Even within the same feature, types that could have been shared were sometimes declared separately.

Some types had gradually become shared dependencies, but their locations still reflected where they were originally created.

As the codebase grew, maintaining those relationships became increasingly difficult.

And there was something else I noticed.

🤖 Types Are Even More Valuable in the AI Era

I've been working with AI-assisted development quite a lot recently.

And through that experience, I've come to appreciate TypeScript's type system even more.

Types aren't just there to catch mistakes.

They can also act as useful guardrails for AI coding agents. 🛤️

Well-defined types help communicate the structure of our applications, the relationships between data, and the contracts our code is expected to follow.

In my experience, making those contracts clear is an important part of building an effective AI development workflow.

But there's a catch!

If our types are duplicated, scattered, or poorly maintained, we're not making full use of that information.

So I started wondering

Why don't we have a tool that helps us maintain the structure of our types automatically?

I looked around, but couldn't find a tool that quite solved the problem I had in mind.

So

I decided to build one myself! 🦀🎉

YAY!!!


🚀 How to Use type-tune

Getting started is pretty simple!

📦 Installation

You can install type-tune from npm

npm install -D type-tune
Enter fullscreen mode Exit fullscreen mode

Or with pnpm (My favorite 😸)

pnpm add -D type-tune
Enter fullscreen mode Exit fullscreen mode

You can also try it without installing it into your project

npx type-tune scan --project tsconfig.json
Enter fullscreen mode Exit fullscreen mode

The npm package requires Node.js v22 or later and runs a platform-specific native Rust binary.

And since type-tune is written in Rust, you can also build and install it directly from GitHub using Cargo! 📦

cargo install --git https://github.com/necoz-oss/type-tune --tag v1.0.0 type-tune
Enter fullscreen mode Exit fullscreen mode

The Rust source requires Rust 1.99 or later.

🔍 Step 1. Find Shared Types

First, scan your TypeScript project

npx type-tune scan --project tsconfig.json
Enter fullscreen mode Exit fullscreen mode

type-tune analyzes your project and finds candidates for relocation.

By default, it looks for exported types and interfaces referenced by at least two other files.

And don't worry!

Scanning doesn't modify your code. 😸

📋 Step 2. Create a Refactoring Plan

Next, generate a plan

npx type-tune plan \
  --project tsconfig.json \
  --out .type-tune/plan.json \
  --markdown .type-tune/plan.md
Enter fullscreen mode Exit fullscreen mode

This creates a JSON plan and a Markdown file you can review.

You can inspect which types were found, where they're currently declared, and where they could be moved.

🐈 Step 3. Review the Changes

Here's an important part!

type-tune doesn't decide to move everything automatically.

Every candidate starts as ignore.

You can review the plan and choose which declarations should actually move.

npx type-tune review \
  --markdown .type-tune/plan.md
Enter fullscreen mode Exit fullscreen mode

You can also edit the Markdown plan manually.

I wanted the tool to automate the tedious work while keeping architectural decisions in the developer's hands.

Automation is useful. But control matters too! 😸

🦀 Step 4. Apply the Changes

Once you've reviewed your plan, you can preview the refactoring

npx type-tune apply \
  --plan .type-tune/plan.json \
  --markdown .type-tune/plan.md \
  --dry-run
Enter fullscreen mode Exit fullscreen mode

type-tune performs the supported rewrites, validates them with TypeScript, and restores the changed files after a normal dry-run.

When you're happy with the plan, run

npx type-tune apply \
  --plan .type-tune/plan.json \
  --markdown .type-tune/plan.md
Enter fullscreen mode Exit fullscreen mode

That's it! 🎉

For supported transformations, type-tune handles moving declarations, updating type imports, and validating the result with the TypeScript compiler.

Of course, you should still review the Git diff and run your project's own checks.

✨ Want to Move Just One Type?

There's also a direct extraction command!

npx type-tune extract OrderSummary \
  --from src/features/order/OrderPage.tsx \
  --to src/features/order/types/order.ts \
  --project tsconfig.json \
  --dry-run
Enter fullscreen mode Exit fullscreen mode

Pretty handy, right? 😸


🐈 Let's Build type-tune Together!

I released type-tune under Necoz because I want to build it together with other developers! 😸💚

Necoz · GitHub

A friendly open-source community building small, simple, and useful tools together. 😸 - Necoz

favicon github.com

And this time, with Rust! 🦀

I believe open source becomes even more exciting when we share ideas, learn from each other, and build things together.

So, if you have any ideas, suggestions, or want to contribute, I'd love to hear from you!

Let's grow type-tune together! 🐈


🌱 Just the Beginning!

This is the first public release, v1!

type-tune currently focuses on exported TypeScript type aliases and interface declarations in .ts and .tsx files.

It doesn't automatically merge duplicate type definitions or handle every possible TypeScript refactoring yet.

But I think it's a useful first step toward making type maintenance easier.

And there's still plenty of room to improve!

We'd love to hear about the problems you've encountered while maintaining types in large TypeScript projects.

Maybe you've run into the same challenges!

Or maybe you've found a completely different approach.

Either way, I'd love to learn from your experience! 💚


💚 Check It Out!

type-tune is open source and available on GitHub!

GitHub logo necoz-oss / type-tune

A Rust-powered CLI to find shared TypeScript types and move them safely. 🦀

type-tune

Animated type-tune logo

Test Docs MIT License

Give shared TypeScript types a better home. 😸

Scan. Review. Move. Validate.

type-tune is a Rust-powered CLI that finds shared TypeScript type aliases and interface declarations and helps move them without manually chasing imports.

It creates a reviewable plan, updates supported type imports and re-exports, and validates the rewrite with TypeScript. You decide what moves and where.

Guides for installation, the first scan, and the review workflow live in the official documentation. Releases are listed on GitHub Releases, and the package is type-tune on npm.

Why type-tune?

Types often start next to a component or utility, then end up shared across a project. Moving them later means tracking imports, local dependencies, and barrels by hand.

  • Discover shared types with deterministic candidate IDs.
  • Review plan.md and choose the destination before making changes.
  • Rewrite supported type-only imports, aliases, and simple barrels while keeping compatibility re-exports at the original…




If you find it interesting, please consider giving it a ⭐ on GitHub! 😸

And if you have any ideas, suggestions, or bugs to report, feel free to open an issue or contribute!

Every bit of feedback helps make these little projects better.

🦀 Coming Up Next

Oh, and one more thing!

You might be wondering

Why did I choose Rust instead of TypeScript to build a TypeScript refactoring tool? Is nyaomaru a TypeScript lover, isn't it? 🤔

That's actually an interesting story!

I'll talk about the reasons behind that decision, what I learned while building it, and my experience using Rust in my next article!

Stay tuned! 🦀

Thanks for reading, and happy coding! 😸🙌


💚 Join Our Necoz Community!

Oh, and one more thing! 😸

We also have a Necoz community on DEV! 🎉

Everyone is welcome, whether you're a developer, a writer, or just curious!

Wanna join? Leave a comment below and I'll send you an invitation! 💌

No pressure, no obligations. Just come hang out with us! 🐈

Small. Simple. Useful. Built Together! 💚

Top comments (25)

Collapse
 
francistrdev profile image
FrancisTRᴅᴇᴠ •

Hey @nyaomaru,

I am here about the DEVengers initiative. What do you say?

cc: @dj29 @hemapriya_kanagala @georgekobaidze @maame-codes

Collapse
 
nyaomaru profile image
nyaomaru Necoz •

@francistrdev

I joined! 😸🙌

Thanks so much for the invitation to DEVengers!

And since we’re inviting each other now, would you like to join Necoz too? 🐈✨

I’d be really happy to have you with us! 😸

Collapse
 
dafili profile image
Renhua Wei •

Structural duplicates don't need a shape hash: the compiler already answers the question bidirectionally. For two type nodes from different files, ask checker.isTypeAssignableTo(a, b) && checker.isTypeAssignableTo(b, a) through the TypeScript compiler API - property order and alias names drop out on their own, while readonly and ? stay in, which is exactly the pair you want kept apart: {a?: string} is not the same type as {a: string | undefined}, the first one accepts a value with no "a" key at all. Gate every merge on that pair being true in both directions and on tsc --noEmit --strict staying green for the call sites after the move.

Collapse
 
nyaomaru profile image
nyaomaru Necoz •

That sounds like a much cleaner approach! 👀

The tricky part I’m thinking about is that two types can have the same shape while representing completely different concepts in the domain.

For example, structurally identical UserId and OrderId-like models might still be intentionally separate.

So even if we can detect structural duplicates reliably, I think type-tune should probably surface them as review candidates, rather than assuming they should be merged automatically.

That would fit the existing philosophy pretty well: automate the tedious discovery, but leave the architectural decision to the developer. 😸

Thanks for the detailed suggestion! This is really useful. 🙌

Collapse
 
dafili profile image
Renhua Wei •

Surfacing rather than merging is right, but make the pair provably distinct or the detector re-reports it every run: one line per nominal type, type UserId = string & { readonly __brand: unique symbol }, and checker.isTypeAssignableTo(userId, orderId) flips to false in both directions, which drops them from the candidates permanently. Keep tsc --noEmit --strict green on the call sites after the brand lands — that is the check that catches a raw string coming in from a DB row.

Thread Thread
 
nyaomaru profile image
nyaomaru Necoz •

That makes sense! 👀

If we use branded types to explicitly mark nominal concepts, then even structurally identical shapes can stay distinguishable, and the detector can naturally stop reporting them as duplicates. That’s a really nice approach. Thanks! 😸🙌

The part I’m still thinking about is how to decide whether two unbranded types actually represent the same domain concept. 😺💭

I’ve been considering an AI-assisted judge/classifier for that part, using things like the type names, surrounding code, usage sites, and maybe file/module context.

But I’m not sure there’s a good deterministic way to answer

“these two shapes are structurally identical, but do they mean the same thing?”

reliably.

Do you have any ideas for heuristics or compiler-level signals that could help with that before falling back to AI? 👀

Thread Thread
 
dafili profile image
Renhua Wei •

Spend the compiler signals before the classifier. For each candidate pair, count the files whose imports reference both symbols and require >=3 shared referrers before a merge is even proposed - zero shared referrers is a hard no-merge that needs no judgement. Two cheap filters remove most of the rest: one-directional assignability (a->b true, b->a false) means a is a narrower type, not a duplicate, and a readonly __brand: unique symbol property in the declaration means nominal by intent, so leave it alone. Whatever survives is a handful of pairs per repo, and that is the only place an AI judge earns its cost.

Collapse
 
gtref profile image
G-type Necoz •

I love this tool. I started using it and I actually have only positive things about it. Though the only thing I could say is will there ever be a version in a language like type script or python so well makes it easier for development instead of downloading rust toolchains and stuff. Just a question.

Collapse
 
nyaomaru profile image
nyaomaru Necoz •

Thank you so much! 😸 I’m really happy to hear you’ve been enjoying type-tune!

I’m definitely planning to keep improving it from here. 🙌

Actually, I was going to write about this in my next article, but type-tune was originally implemented in TypeScript first, and I later migrated it to Rust. 😹

Python support might be worth exploring someday too!

For now, though, I’m planning to keep the core in Rust. If you install type-tune through npm, you don’t need to set up a Rust toolchain yourself since the package uses a prebuilt binary.

Thanks a lot for the suggestion! I really appreciate it. 😸🦀

Collapse
 
gtref profile image
G-type Necoz •

I also started my own fork. Though it has not changed much I am hoping to leave it a little better than I found it.

Thread Thread
 
nyaomaru profile image
nyaomaru Necoz •

That’s awesome to hear! 😸🙌

Even small improvements are always welcome, so if you notice anything along the way, feel free to open an issue or PR anytime!

I’m really curious to see where you take it. 😸

Collapse
 
pjdeveloper896 profile image
Prasoon Jadon Necoz •

Really interesting project! I like how type-tune focuses on a problem that often gets overlooked: maintaining and organizing shared types as a codebase grows. The review-first approach is especially nice because automation shouldn't take architectural control away from developers.

Collapse
 
nyaomaru profile image
nyaomaru Necoz •

Thank you so much! 😸🙌

That’s exactly the philosophy behind type-tune.

I want to automate the tedious parts of refactoring, but keep architectural decisions in the developer’s hands. 🤚

I’m really glad the review-first approach resonated with you! 🐈🦀

Collapse
 
pjdeveloper896 profile image
Prasoon Jadon Necoz •

I'm glad too! After learning more about your project, I'm even more excited. Looking forward to exploring type - tune and contributing to it! 😸💚

Collapse
 
csm18 profile image
csm •

Awesome!

Collapse
 
nyaomaru profile image
nyaomaru Necoz •

Thx! 😸🦀

Collapse
 
mp3sss31 profile image
mp3sss •

The reviewable plan.md step is what makes a type refactor safe rather than a blind rewrite — chasing shared aliases and barrels by hand is exactly where a mechanical mover can introduce subtle breakage, so validating the rewrite against TypeScript before committing is a good guardrail. In a desktop app codebase this kind of tooling pays off especially well across main/preload/renderer shared contracts, where a duplicated payload type tends to drift the moment two sides edit it independently. One thing worth checking in a larger repo: how the mover handles conditional and mapped types that reference local generics, since those are the cases a plain alias/interface pass can miss.

Collapse
 
nyaomaru profile image
nyaomaru Necoz •

That’s a great point! 👀

I created an issue for this so I can investigate the edge cases properly and add coverage for conditional types, mapped types, and local generic dependencies.

I’ll work through it from there and make sure type-tune handles these cases safely. 😸🙌

Thanks again for bringing it up!

Collapse
 
ivannovazzi profile image
Ivan Annovazzi •

The part about devs re-declaring similar types hits hard, AI-generated code does this constantly because it can't see what already exists in the repo. Does it catch types with different names but identical shapes, or only same-name duplicates? That second case is where I'd want this the most

Collapse
 
nyaomaru profile image
nyaomaru Necoz •

Yes, that AI-generated duplication problem is exactly the kind of thing I’ve been thinking about too. 😹

For v1, type-tune actually doesn’t detect duplicate definitions yet, either by name or by structural shape.

Right now it focuses on types that are already being shared across multiple files, then helps identify and move them to a better location safely.

But detecting differently named types with identical or very similar shapes is definitely an interesting next direction. 👀

Something like 👇

type User = { id: string; name: string };
type Customer = { id: string; name: string };
Enter fullscreen mode Exit fullscreen mode

could be especially useful to surface when AI keeps creating new types instead of reusing what already exists.

That’s a really good use case. I’d love to explore this further! 😸🙌

Collapse
 
fattakhov profile image
Mansur Fattakhov •

What happens if the source changes between reviewing a plan and applying it? Does apply check that the declarations and imports still match the reviewed version and stop on a mismatch? That seems useful when a plan sits in a branch while other changes land.

Collapse
 
nyaomaru profile image
nyaomaru Necoz •

Yes! That’s exactly one of the cases the plan is designed to protect against. 😸

When apply runs, type-tune re-scans the project and compares the current candidate metadata against what was stored in the reviewed plan.

So if the referrers or import strategies have changed while the plan was sitting around, the candidate no longer matches and apply stops and asks you to regenerate the plan.

It also stores a fingerprint of the source declaration and its same-file type dependencies. If any of those declarations changed after the plan was created, the plan is treated as stale and won’t be applied. 👍

And after the rewrite, TypeScript validation still runs as the final guardrail, with rollback if validation fails.

So a plan can safely sit on a branch, but if relevant code changes underneath it, type-tune should refuse to blindly apply the old plan. 👀

Really good question! This is exactly the kind of scenario I wanted the review-first workflow to handle. 😸🙌

Collapse
 
parasraju profile image
Paras •

The reviewable plan before moving types is clever.

Collapse
 
nyaomaru profile image
nyaomaru Necoz •

Thank you! 😸🙌

I came up with the reviewable plan because I’ve had a few refactors where I just ran the automation first, checked the diff afterward, and then realized, “Ah… maybe this one shouldn’t have been changed after all.” 😹

So with type-tune, I wanted the decision to happen before the rewrite, not after it.

The tool can do the mechanical work, but the developer should still get the final say. 🐈

Collapse
 
danielo515 profile image
Daniel Rodríguez Rivero •

Having duplicate types is not always bad. Sometimes you want separate domains where the same type will evolve differently, and making those shared doesn't help anyone. A client to one domain would not be the same as a client in another domain, even if they now share some properties.