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! π¦π
type-tune
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.mdand 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;
};
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.
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
Or with pnpm (My favorite πΈ)
pnpm add -D type-tune
You can also try it without installing it into your project
npx type-tune scan --project tsconfig.json
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
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
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
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
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
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
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
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! πΈπ
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!
type-tune
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.mdand 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 (23)
Hey @nyaomaru,
I am here about the DEVengers initiative. What do you say?
cc: @dj29 @hemapriya_kanagala @georgekobaidze @maame-codes
@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! πΈ
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.
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
UserIdandOrderId-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. π
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 }, andchecker.isTypeAssignableTo(userId, orderId)flips to false in both directions, which drops them from the candidates permanently. Keeptsc --noEmit --strictgreen on the call sites after the brand lands β that is the check that catches a rawstringcoming in from a DB row.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
reliably.
Do you have any ideas for heuristics or compiler-level signals that could help with that before falling back to AI? π
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 symbolproperty 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.Awesome!
Thx! πΈπ¦
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.
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. πΈπ¦
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.
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. πΈ
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
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 π
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! πΈπ
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.
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!
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.
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! ππ¦
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.
Yes! Thatβs exactly one of the cases the plan is designed to protect against. πΈ
When
applyruns,type-tunere-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
applystops 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-tuneshould 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. πΈπ
The reviewable plan before moving types is clever.
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. π