Полунадуманный, полуподсмотренный вариант может выглядеть так.
0) Не тащим типы в рантайм, дженерики стираются. Для обратной совместимости байткода
1) Тип Pid имеет тайп конструктор. Для начала пусть принимает тип принимаемого сообщения. Pid[MyMessage]
2) Об ADT говорили выше. Простите за эликсир, возьму оттуда структурки, сделаю тип сумму и навешу тайп алиас
type MyMessage = MyStruct1 | MyStruct2 | MyStruct3
3) spawn расширяется на тип принимаемого сообщения. spawn(MyMessage, fn -> end)
4) С этого момента в компайлтайме невозможно отправить актору сообщение не из MyMessage
5) В скомпилированном виде не поменялось ничего, Pid[MyMessage] стал обычным pid
6) Добавление нового вида сообщения в общий тип роняет компиляцию, если не распаттернматчил его в кейсе. Потому что становится возможным и очень просто посчитать все возможные типы