What Is a Type Alias in Go (type X = Y)

A type alias in Go creates a new name for an existing type using the `type X = Y` syntax, allowing interchangeable use without defining a new distinct type.

When a name change shouldn't break the type system

You are refactoring a codebase. You decide that int is too vague for user identifiers, so you introduce a UserID type. You update the domain logic, but suddenly fifty functions break. The database layer expects int. The API handler expects int. You didn't change the underlying data, just the name, and the compiler treats UserID as a completely foreign type.

You need a way to rename a type without triggering a cascade of type mismatches. You want to introduce the new name gradually, or you simply want a shorter name for a verbose generic type. A type alias gives you a synonym. The compiler treats the alias and the original type as identical, so your code compiles while you refactor.

Alias versus definition

Go has two ways to create a new name using the type keyword. They look similar but behave very differently.

A type alias uses an equals sign: type Alias = Original. This creates a synonym. Alias and Original are the exact same type. Variables, function parameters, and return values are interchangeable. The compiler substitutes the alias with the original type everywhere.

A type definition omits the equals sign: type NewType Original. This creates a distinct type. NewType has the same underlying structure as Original, but the compiler treats it as a separate identity. You cannot pass a NewType to a function expecting Original without an explicit conversion. You can add methods to NewType. You cannot add methods to an alias.

Think of an alias as a nickname. If your name is David and your nickname is Dave, everyone knows Dave is David. A type definition is like creating a character named Dave who looks exactly like you. The character is distinct from you, even if they share your appearance.

Minimal example

Here is the simplest alias: a new name for an existing type that works everywhere the original type works.

package main

import "fmt"

// AliasName is just another name for string.
// The compiler treats AliasName and string as identical.
type AliasName = string

// printValue accepts a string.
func printValue(s string) {
    fmt.Println(s)
}

func main() {
    // varName holds a string, declared using the alias.
    varName := AliasName("hello")

    // This works because AliasName is string.
    // No conversion is needed.
    printValue(varName)

    // You can also assign a string directly to the alias.
    varName = "world"
}

The code compiles and runs without friction. AliasName is not a wrapper. It is not a struct containing a string. It is a string. The type system sees string wherever it sees AliasName.

How the compiler handles aliases

When the compiler encounters type Alias = Original, it performs a substitution. There is no new type created in the type system. The alias is erased during compilation.

This has consequences for methods. You cannot add a method to a type alias. Methods belong to the type, not the name. If you try to define a method on an alias, the compiler rejects the program.

package main

// AliasInt is an alias for int.
type AliasInt = int

// This fails. You cannot add methods to an alias.
// The compiler rejects this with:
// cannot define new methods on non-local type int
func (a AliasInt) Double() int {
    return a * 2
}

The error message is direct. The alias does not own the type. It points to int, and int is a built-in type defined in the standard library. Go only allows methods on types defined in the current package. Since AliasInt is just int, the compiler sees an attempt to add a method to int, which is forbidden.

If you need methods, you must use a type definition. A definition creates a new type that lives in your package, and you can attach methods to it.

Aliases also satisfy interfaces exactly as the underlying type does. If string implements an interface, AliasName implements it too. There is no gap. The alias is transparent to interface satisfaction.

Realistic example: generic shortcuts

Type aliases shine when working with generics. Generic types can have verbose instantiations. An alias lets you define a convenient shorthand for a common usage pattern.

Here is a generic result type, followed by an alias for a specific instantiation.

package main

import "fmt"

// Result wraps a value and an error for a generic type T.
type Result[T any] struct {
    Value T
    Err   error
}

// StringResult is an alias for Result[string].
// This saves typing Result[string] in function signatures.
type StringResult = Result[string]

// fetchConfig returns a StringResult.
// The return type is readable and concise.
func fetchConfig() StringResult {
    return StringResult{
        Value: "config.yaml",
        Err:   nil,
    }
}

func main() {
    // res uses the alias.
    res := fetchConfig()

    // Access fields directly.
    // The alias resolves to Result[string].
    fmt.Println(res.Value)
}

The alias StringResult makes the code cleaner. Function signatures read better. You avoid repeating Result[string] everywhere. The alias is defined in the package, so any code in the package can use it. The compiler substitutes StringResult with Result[string] during type checking.

This pattern is common in libraries. A library might define a complex generic type and provide aliases for the most useful instantiations. Users of the library get a convenient API without sacrificing type safety.

Realistic example: migration

Aliases are the standard tool for renaming types across a large codebase. You can introduce the new name, use it alongside the old name, and eventually remove the old name.

Suppose you have a package that uses int for durations. You want to switch to a Duration type to prevent mixing up durations with counts. You cannot change every file at once. You create an alias for the old type and gradually replace it.

package main

import "fmt"

// Duration is the new type for time values.
// This is a distinct type to enforce type safety.
type Duration int

// Seconds is an alias for int.
// Existing code uses int; this alias documents the intent
// without breaking type compatibility.
type Seconds = int

// addDuration takes the new Duration type.
func addDuration(d Duration) {
    fmt.Println("Added duration:", d)
}

func main() {
    // oldTime uses the alias.
    // This is effectively an int.
    oldTime := Seconds(10)

    // newTime uses the distinct type.
    newTime := Duration(20)

    // This fails. Seconds is int, Duration is a distinct type.
    // The compiler rejects this with:
    // cannot use oldTime (type Seconds) as type Duration in argument
    // addDuration(oldTime)

    // You must convert explicitly.
    // The alias helps readability, but doesn't bridge distinct types.
    addDuration(Duration(oldTime))
}

The alias Seconds documents that the int represents seconds. It improves readability. It does not bridge the gap to Duration. Duration is a distinct type, so you still need a conversion. The alias is a stepping stone. You can rename int to Seconds in old code, then later change Seconds to Duration where appropriate.

This migration pattern works best when you rename a type to another type. If you alias OldType to NewType, and NewType is a definition, the alias points to the definition. You can use the alias in new code, and it works with the definition. Eventually, you remove the alias and use the definition directly.

Pitfalls and compiler errors

Type aliases are simple, but a few traps exist.

Confusing an alias with a definition is the most common mistake. The equals sign is the difference. type A = B is an alias. type A B is a definition. If you forget the equals sign, you create a distinct type. Your code might compile, but you'll get type mismatch errors where you expected compatibility. The compiler complains with cannot use x (type A) as type B in argument if you try to pass the definition where the original type is expected.

Self-referential aliases are illegal. You cannot write type A = A. The compiler rejects this with type A is aliased to itself. The alias must point to a different type.

Aliases are scoped to the package. You cannot import an alias from another package. If package P defines type Alias = int, package Main sees Alias as int. The name Alias does not exist in Main. You must use int or define your own alias. This prevents name collisions and keeps the namespace clean.

Built-in aliases exist for historical reasons. byte is an alias for uint8. any is an alias for interface{}. These are defined in the builtin package. You should use byte when working with bytes and uint8 when working with numeric values. You should use any in modern Go code instead of interface{}. The aliases are idiomatic and improve readability.

Decision matrix

Use a type alias when you want to rename a type without breaking existing code that expects the original type. Use a type alias when you need a shorter name for a verbose generic type instantiation. Use a type definition when you want to create a distinct type with its own methods or to enforce type safety between similar values. Use the built-in aliases byte and any when writing idiomatic Go code, as they are the standard names for uint8 and interface{}.

Aliases rename. Definitions redefine. Pick the tool that matches your goal.

Where to go next