Solo developer building practical CLI tools for Linux auditing and B2B data extraction. Creator of Vaultcheck and NPILift. I write about silent failures, false green lights, and infrastructure reliabi
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.
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. 🙌
Solo developer building practical CLI tools for Linux auditing and B2B data extraction. Creator of Vaultcheck and NPILift. I write about silent failures, false green lights, and infrastructure reliabi
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.
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? 👀
Solo developer building practical CLI tools for Linux auditing and B2B data extraction. Creator of Vaultcheck and NPILift. I write about silent failures, false green lights, and infrastructure reliabi
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.
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
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! 🙌
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. 😸🦀
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.
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
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.
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. 😸🙌
CTO & software architect, 15+ years in backend and infrastructure. Writing about bringing order to projects: observability, architecture, teams, processes. Blog: mind.mansur.expert
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 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. 😸🙌
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.
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. 🐈
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. 🐈
We're a place where coders share, stay up-to-date and grow their careers.
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! 😸
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.
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! 🎉
Joined! Thanks for invite :D
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.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
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! 🙌
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. 😸
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! 🐈🦀
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! 😸💚
Awesome!
Thx! 😸🦀
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!
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. 😸🙌
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.
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 → applyworkflow 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. 🐈
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. 🐈