What’s the meaning of the new tilde token ~ in Go?

In Go generics, the ~ tilde token is used in the form ~T to denote the set of types whose underlying type is T.

It was also called “approximation” constraint element in the generics proposal, which explains what it’s good for in plain language:

Listing a single type is useless by itself. For constraint satisfaction, we want to be able to say not just int, but “any type whose underlying type is int”. […] If a program uses type MyString string, the program can use the < operator with values of type MyString. It should be possible to instantiate [a function] with the type MyString.

If you want a formal reference, the language spec has placed the definition of underlying types in its own section:

Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T’s underlying type is the underlying type of the type to which T refers in its type declaration.

This covers the very common cases of type literals and other composite types with bound identifiers, or types you define over predeclared identifiers, which is the case mentioned in the generics proposal:

// underlying type = struct literal -> itself -> struct { n int }
type Foo struct {
    n int
}

// underlying type = slice literal -> itself -> []byte
type ByteSlice []byte

// underlying type = predeclared -> itself -> int8
type MyInt8 int8

// underlying type = predeclared -> itself -> string
type MyString string

The practical implication is that an interface constraint whose type set has only exact elements doesn’t allow your own defined types:

// hypothetical constraint without approximation elements
type ExactSigned interface {
    int | int8 | int16 | int32 | int64
}

// CANNOT instantiate with MyInt8
func echoExact[T ExactSigned](t T) T { return t }

// constraints.Signed uses approximation elements e.g. ~int8
// CAN instantiate with MyInt8
func echo[T constraints.Signed](t T) T { return t }

As with other constraint elements, you can use the approximation elements in unions, as in constraints.Signed or in anonymous constraints with or without syntactic sugar. Notably the syntactic sugar with only one approx element is valid:

// anonymous constraint
func echoFixedSize[T interface { ~int8 | ~int32 | ~int64 }](t T) T { 
    return t 
}

// anonymous constraint with syntactic sugar
func echoFixedSizeSugar[T ~int8 | ~int32 | ~int64](t T) T { 
    return t 
}

// anonymous constraint with syntactic sugar and one element
func echoFixedSizeSugarOne[T ~int8](t T) T { 
    return t 
}

As anticipated above, a common use case for approximation elements is with composite types (slices, structs, etc.) that need to have methods. In that case you must bind the identifier:

// must bind identifier in order to declare methods
type ByteSeq []byte

func (b ByteSeq) DoSomething() {}

and now the approximation element is handy to allow instantiation with ByteSeq:

// ByteSeq not allowed, or must convert func argument first
func foobar[T interface { []byte }](t T) { /* ... */ }


// ByteSeq allowed
func bazquux[T interface { ~[]byte }](t T) { /* ... */ }

func main() {
    b := []byte{0x00, 0x01}
    seq := ByteSeq{0x02, 0x03}

    foobar(b)           // ok
    foobar(seq)         // compiler error
    foobar([]byte(seq)) // ok, allows inference
    foobar[[]byte](seq) // ok, explicit instantiation, then can assign seq to argument type []byte

    bazquux(b)          // ok
    bazquux(seq)        // ok
}

NOTE: you can not use the approximation token with a type parameter:

// INVALID!
type AnyApprox[T any] interface {
    ~T
}

Leave a Comment