skip to content

When you move a widely imported type to a new package, how do you decide between leaving an alias behind and making callers migrate?

level: principalimportance: should knowfreq 35%

answer

  1. who do you not control?
  2. can the change land atomically?
  3. identical type versus a different one
  4. a shim needs an owner and a delete date

basics

~20 s

Leave an alias when you cannot change every importer in one commit: it keeps the old and new names identical, so migrated and unmigrated code interoperate. Otherwise move the type and delete the old name.

solid answer

~50 s

The mechanism decides the options. `type old.T = new.T` is an alias, so both names denote one type: values, channels and exported signatures on either side stay compatible, and importers migrate on their own schedule. If the new package instead declares its own defined type, it is a *different* type — every value crossing the boundary needs a conversion and every shared signature breaks at once, so the move becomes a flag day. So I decide on reach and coordination: how many importers I do not control, whether I can land one atomic change, whether the type appears in exported signatures other teams implement, and whether the old package must keep compiling through a release window. If it is all one repository and the change lands in one green commit, I move the type and delete the old name. Any alias I do leave ships with an owner, a deprecation comment and a removal release. An alias with no exit date is permanent API by accident.

code

go · 9 lines
go
package oldpkg

// Sample is moved to newpkg.
//
// Deprecated: use newpkg.Sample. This alias is removed in v2.0.
type Sample = newpkg.Sample

// Generic types can be shimmed the same way since Go 1.24.
type Series[T any] = newpkg.Series[T]

go deeper

for a junior

Know the mechanism you would be asked to use: an alias in the old package keeps the old name compiling because both names mean the same type.

for a middle

Explain precisely why the alias keeps callers compiling while a separately defined type in the new package would not, including what happens to shared interfaces and channel element types.

for a senior

Plan the migration: leave the shim, mark it deprecated, track remaining importers to zero, and delete it in a named release rather than letting it drift.

for a principal

Own the call and its consequences - reach, atomicity, release windows, and the fact that an unowned alias becomes permanent API and takes the type's evolution out of the old package's hands.

## Why this is a decision and not a mechanic Moving a type between packages looks like a rename, but the choice of *what to leave behind* determines whether other teams get a gradual migration or a flag day, and who carries the cost. ### What the alias actually buys `type T = newpkg.T` in the old package creates no type. The old and new spellings are one type, which means: - code that has migrated and code that has not can pass values to each other; - a shared interface, channel or map declared with either spelling matches the other; - third parties implementing your interfaces are unaffected; - nothing needs converting, at any boundary, ever. That is what makes a package move *incremental*. Since Go 1.24 the same shim works for generic types, written `type T[K comparable] = newpkg.T[K]`. ### What a defined type in the new package costs If the new package declares `type T struct{...}` afresh and the old one keeps its own, they are two types with the same shape and no relationship. Every value crossing the boundary needs an explicit conversion; every function signature, channel element type and interface method that mentions the type stops matching; and any external implementation of an affected interface breaks. This is a coordinated cut-over, and it is only sane when you can land it in one change. ### The inputs I actually weigh 1. **Reach.** How many importers exist and how many are outside my control? One repository I can change atomically is a completely different problem from an SDK consumed by teams on their own release trains. 2. **Atomicity.** Can the move land as a single commit that builds green? If yes, prefer no shim: mechanical rewriting tools handle the renaming and the tree is left with one name for one thing. 3. **Surface shape.** Does the type appear in *exported* signatures other people implement or satisfy? Interfaces and channel element types are the sharp edges; a type used only internally is much cheaper to cut over. 4. **Release window.** Must the old package keep compiling through a support window or a rollback? If a rollback has to build, the shim is not optional. 5. **Ownership.** Who writes the migration guide, who chases the remaining importers, and who deletes the alias? If the answer is nobody, I am not choosing between a shim and a cut-over; I am choosing between a cut-over and permanent duplication. ### The costs of the alias I make sure the team sees - **It becomes permanent.** An alias with no removal date is API. In a year the old import path is still in half the code and the migration is a fiction. - **The doc surface doubles.** Two names, two places a reader lands, and generated documentation lists both. - **Indirection.** Go-to-definition hops through the alias, and readers of the old package no longer see the type's real declaration or its methods. - **The old package loses the ability to evolve the type.** Once the name is an alias, the old package cannot declare methods on it — the receiver type now belongs to another package — so every change is the new owner's call. - **A migration that only ever half-happens is worse than either end state**, because reviewers must now know which spelling is current. ### How I run it when the alias is the right call Ship the alias with a deprecation comment naming the replacement, a target release for deletion, and a named owner. Announce it with a short migration note that says exactly what to change and what does not change (nothing at run time — the types are identical). Track remaining importers with a repository search or a dependency listing, drive the number to zero, then delete the alias in the release you promised. Recent Go also lets you mark an alias with a `//go:fix inline` comment so `go fix` rewrites uses of the old name to the new one, which turns most of the migration into a mechanical step consumers can run themselves. The principle underneath: an alias is a *transition* tool. It is cheap precisely because it is temporary, and it stops being cheap the moment nobody owns its removal.

  • What are the exit criteria for the alias, and who owns them?
    A named owner, a target release for deletion, a deprecation comment pointing at the replacement, and a countable signal — remaining importers found by repository search or dependency listing driven to zero. Without those four, the alias is not a migration step; it is a second permanent name for the type, and reviewers will forever have to know which spelling is current.
  • What exactly breaks if the new package declares its own type instead of an alias?
    The two types are unrelated, so every value crossing the boundary needs a conversion, every exported signature, channel element type and map type that mentions it stops matching, and any external implementation of an affected interface fails to compile. It turns an incremental migration into a coordinated flag day across every consumer at once.
  • What can the old package no longer do once its exported name is an alias?
    It cannot declare methods on that name, because the receiver type is now declared in another package, and it cannot evolve the type independently — fields, methods and semantics are all the new owner's call. That is usually correct for a move, but it means the old package has surrendered the type, not merely relocated it.
  • When is the answer simply to move it and update every caller?
    When the code lives in one repository, the change builds green as a single commit, and no consumer outside it pins an old version. Then a shim buys nothing and costs a name that lingers: mechanical rewriting handles the renaming, review sees one atomic diff, and the tree ends with exactly one name for the type.

saying these in an interview costs you the question

  • Treats an exported alias as permanent API with no removal plan
  • Ignores importers outside the repository they can see
  • Claims callers can just convert, as if conversions were free
  • Cannot name who deletes the shim or when
  • Thinks an alias costs something at run time