Ao usar protocolos do Swift, você inevitavelmente encontra uma barreira: erros de tipos associados ao tentar usar Equatable como tipo de variável ou armazenar Collection em uma propriedade. O já infame “Protocol can only be used as a generic constraint because it has Self or associated type requirements” é o principal exemplo. A identidade dessa barreira são os tipos associados (associatedtype).
Esta é a quarta parte da série intermediária. Vamos revisar o que são tipos associados, por que aquele erro ocorria e a evolução da linguagem até os primary associated types. Se você já leu as partes sobre generics e some·any, todos os conceitos necessários já estão preparados.
O que são tipos associados — Espaços reservados para tipos em protocolos
Na parte sobre generics, <T> foi descrito como um “espaço para preencher o tipo depois”. Um tipo associado é esse espaço reservado dentro de um protocolo.
Imagine que vamos criar um protocolo de contêiner. Queremos abstrair a capacidade comum de “inserir e remover elementos” de pilhas e filas, mas o problema é o tipo do elemento. Os elementos de IntStack são Int, enquanto os de StringQueue são String, então o protocolo não pode definir o tipo antecipadamente. Por isso, declaramos um espaço reservado.
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
O tipo que adota o protocolo preenche o espaço. IntStack usa Int para Item, e StringQueue usa String. Na maioria dos casos, nem é preciso escrever typealias Item = Int explicitamente: o compilador infere isso pelo tipo do parâmetro de append.
Na verdade, não é um conceito novo. A estrutura da biblioteca padrão é toda baseada em tipos associados. Element e Index de Collection, Element de IteratorProtocol e C.Element, visto nas cláusulas where da parte sobre generics, apontam para esse espaço reservado. Até Equatable tem um requisito Self (static func == (lhs: Self, rhs: Self) -> Bool), primo dos tipos associados. Em resumo, tipos associados são cidadãos de primeira classe no mundo dos protocolos.
A diferença para <T> de generics pode ser resumida assim: o parâmetro genérico é preenchido por quem usa, enquanto o tipo associado é preenchido por quem adota. Stack<Int> permite que o usuário escolha Int, mas Item de Container é definido pelo próprio tipo IntStack.
Por que o erro ocorria — Não é possível definir a especificação da caixa
Agora podemos dissecar aquele erro infame. Por que um protocolo com tipo associado não podia ser usado como tipo?
Na parte sobre some·any, dissemos que uma variável de tipo protocolo é um tipo existencial, ou seja, uma caixa. Escrever var c: Container significa criar uma caixa contendo algo que conforma a Container. O problema surge ao remover um elemento da caixa. Qual é o tipo de c[0]? É Item, mas isso depende do que realmente está dentro da caixa. Em IntStack é Int; em StringQueue, String. O compilador não consegue responder olhando apenas para a caixa.
Uma expressão cujo tipo não pode ser determinado não é permitida em uma linguagem de tipos estáticos, então as versões antigas do Swift a bloqueavam logo na entrada. O erro realmente queria dizer: “Use este protocolo apenas como restrição”. Com o genérico <C: Container>, C é definido no momento da chamada, assim como C.Item, e não há problema. A mensagem era pouco amigável, mas a solução — usar um genérico em vez de uma caixa — era precisa.
Evolução da linguagem — A história de como as travas foram abertas
Esse inconveniente ficou famoso por muito tempo, e o Swift foi removendo as travas por meio de várias propostas.
Swift 5.7, proposta Swift Evolution SE-0309. Protocolos com tipos associados passaram a poder ser usados com any. var c: any Container compila. Porém, o Item retirado da caixa é tratado como “um tipo desconhecido”, o que limita seu uso. A porta está aberta, mas ainda há pouco a fazer lá dentro.
Na mesma época, o SE-0346 introduziu os primary associated types. A verdadeira virada foi esta: tornou-se possível expor os principais tipos associados na declaração do protocolo, entre colchetes angulares.
protocol Container<Item> {
associatedtype Item
// ...
}
var numbers: any Container<Int> // Itemum Int contêiner
func process(_ c: some Container<Int>) // De forma concisa também em generics
any Container<Int> é uma “caixa cujo Item foi definido como Int”. O compilador sabe que o elemento retirado é Int, então a utilidade da caixa aumenta drasticamente. A biblioteca padrão também foi reorganizada com essa sintaxe. Foi a partir daí que formas como any Collection<String> e some Sequence<Int> passaram a ser possíveis.
Vale entender a direção dessa evolução. A regra absoluta “um protocolo com tipos associados não pode ser usado como tipo” tornou-se a regra precisa “ele pode ser usado, mas suas capacidades diminuem conforme os tipos associados não definidos”. É uma extensão da filosofia vista em some·any: da proibição à explicitação do custo.
Padrões práticos — Como trabalhar com tipos associados
Vamos levar a teoria para situações práticas.
Ao projetar: use um tipo associado quando “cada tipo adotante define um tipo diferente”. Um protocolo Repository é um bom exemplo. Com associatedtype Entity, UserRepository é preenchido com User, e OrderRepository com Order. Se quem usa deve escolher o tipo, independentemente do adotante, o correto é uma função ou tipo genérico.
Ao consumir: a opção padrão são restrições genéricas; se for necessário armazenar e misturar, use any com o primary associated type preenchido. Usar uma restrição como func sync<R: Repository>(_ repo: R) where R.Entity == User é a primeira opção; armazenar como var repos: [any Repository<User>] é a segunda.
Quando ficar bloqueado: use type erasure como último recurso, o padrão AnyX. Se nem o primary associated type resolver restrições complexas, resta a type erasure manual: ocultar o tipo associado envolvendo-o em um tipo concreto, como AnySequence da biblioteca padrão ou AnyPublisher do Combine. Desde o SE-0346, isso é necessário com muito menos frequência. Antes de criar um novo wrapper AnyX, verifique primeiro se o primary associated type resolve o problema.
Mais um ponto: requisitos Self. Operações como == só fazem sentido “entre valores do mesmo tipo”, por isso são declaradas com Self. Consequentemente, duas caixas any Equatable não podem ser comparadas diretamente, pois os tipos internos podem ser diferentes. Nesses casos, o padrão é abandonar a caixa e resolver com generics.
Resumo
- Tipos associados são espaços reservados para tipos em protocolos, preenchidos pelos tipos adotantes, geralmente por inferência. A biblioteca padrão é construída sobre eles, como no Element de Collection.
- A origem do erro antigo: a entrada era bloqueada porque não era possível definir o tipo associado do valor retirado de uma caixa existencial. Com restrições genéricas, isso nunca foi um problema.
- SE-0309 liberou o uso de any, e os primary associated types do SE-0346 (
any Container<Int>) tornaram as caixas práticas. - Ordem prática: primeiro, restrições genéricas; depois, any com o primary associated type preenchido; por fim, type erasure manual.
O próximo artigo aborda um tema mais prático: map, filter, reduce e as diferenças entre compactMap e flatMap, além de avançar até desempenho com sequências lazy.
Referências
- SE-0309: Unlock existentials for all protocols
- SE-0346: Lightweight same-type requirements for primary associated types

![Imagem de capa de [Swift intermediário #4] Dominando os tipos associados do Swift (associatedtype)](/assets/images/posts/843650cf-8f58-411e-9ef4-14639a9f6490/swift-associatedtype-1.jpg)