DEV Community

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

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

nyaomaru on November 11, 2026

Hoi hoi! 👋 I'm @nyaomaru, a frontend engineer who is actually allergic to crabs! 🙀🦀 Today, I have another exciting announcement! I've just relea...
Collapse
 
francistrdev profile image
FrancisTRᴅᴇᴠ Necoz •

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
 
francistrdev profile image
FrancisTRᴅᴇᴠ Necoz •

Awesome and would love to join! I do prefer secret code via email if possible since I am more conformable than clicking on a link.

Thread Thread
 
nyaomaru profile image
nyaomaru Necoz •

Awesome! 😸
I totally understand! With all the security vulnerabilities we've been seeing lately, it's better to be cautious!

I've just sent you an email with the secret code, so please check your inbox! 💚

Looking forward to having you join Necoz! 🎉

Thread Thread
 
francistrdev profile image
FrancisTRᴅᴇᴠ Necoz •

Joined! Thanks for invite :D

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.

Thread Thread
 
nyaomaru profile image
nyaomaru Necoz •

Actually, this is very similar to an approach I’ve been using in changelog-bot too: do as much as possible deterministically first, and only use AI for the smaller ambiguous part that remains. 😸

So I really like the idea of applying the same philosophy here. 👍

Shared referrers seem especially useful as a practical signal. If two structurally similar types are never used together anywhere, that’s already a pretty strong reason not to suggest merging them.

And filtering out one-way assignability and explicitly branded types before classification also feels very clean.

So the flow could become something like

structural candidate → usage/referrer signals → assignability/nominal filters → only then AI judgement
Enter fullscreen mode Exit fullscreen mode

That would keep the AI part small and focused instead of asking it to reason about every pair in the repository. 😸

This is really helpful. I think this is much closer to the direction I want to take for the duplicate detection work. Thanks again! 🙌

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
 
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
 
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
 
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
 
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.

Collapse
 
nyaomaru profile image
nyaomaru Necoz •

Exactly! 😸

Having duplicate types is not automatically a problem, especially when they belong to different domains and may evolve independently.

That’s one of the main reasons type-tune follows a scan → plan → apply workflow instead of blindly changing everything it finds.

The tool is meant to surface candidates and handle the mechanical part of moving them, but the developer still decides whether a type should actually be shared.

So no, I definitely don’t expect people to extract every structurally similar or reused type into one common place. 👀

Sometimes keeping two types separate is the correct architectural decision. 🐈

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. 🐈