Why do type assertions on interface type aliases fail?

When you create two type aliases for string for example, type assertions correctly distinguish between the different type aliases.

package main

import (
	"fmt"
)

type Type1 string
type Type2 string

func main() {
	var v any = Type1("hello")

	checkType[Type1]("Type1", v)
	checkType[Type2]("Type2", v)
}

func checkType[T any](name string, v any) {
	_, ok := v.(T)
	fmt.Printf("type is %s: %t\n", name, ok)
}

// Outputs:
// type is Type1: true
// type is Type2: false

But when creating type aliases for interfaces like error, the type assertions fail.

package main

import (
	"errors"
	"fmt"
)

type Type1 error
type Type2 error

func main() {
	var v any = Type1(errors.New("hello"))

	checkType[Type1]("Type1", v)
	checkType[Type2]("Type2", v)
}

func checkType[T any](name string, v any) {
	_, ok := v.(T)
	fmt.Printf("type is %s: %t\n", name, ok)
}

// Outputs:
// type is Type1: true
// type is Type2: true

Why does the type assertion fail when aliasing interface types but with specific types, it does not?

I ran into this issue multiple times when building more complex error handlings based on error types and error type assertions. My solution was mostyl to create a struct which wraps the error and which implements error and Unwrap(). How do you distinguish between different error types in your code base?

Would be really interested in an explanation why Go behaves this way and what is an “idiomatic” solution to this.

Hello there. There is no error here, go behaves as it should. You are trying to compare type string and the error interface. To suffice interface, the type simply should implement all the methods of the interface. If you add, for example with errors, more complex type, such as struct with Error() method, it will also show true on Type1 and Type2, since it will follow the error interface.

There are functions As and Is in errors package. Have you tried to use them for your purposes? They’ve proved their use when I was writing my own errors handlers, depending on their actual type.

The important disctintion is what v.(T) does. If T is a struct type, go will check if v is of that type. If T is an interface go will check if v can fulfill that interface. This is an important feature of go - since you can define your own interface - and every type, which has the relevant methods can be cast to your interface, even if the type is from a completely unrelated code base.

So everything which has an Error() method will fulfill your Type1 and Type2 interfaces, since an interface is just a blueprint, which any type can fulfill.