O sistema de tipos do Dart
Por que e como escrever código Dart sound.
A linguagem Dart é type safe: ela usa uma combinação de verificação estática de tipos e verificações em tempo de execução para garantir que o valor de uma variável sempre corresponda ao tipo estático da variável, algo às vezes chamado de tipagem sound. Embora os tipos sejam obrigatórios, as anotações de tipo são opcionais por causa da inferência de tipos.
Um dos benefícios da verificação estática de tipos é a capacidade de encontrar bugs em tempo de compilação usando o analisador estático do Dart.
Você pode corrigir a maioria dos erros de análise estática adicionando anotações de tipo a
classes genéricas. As classes genéricas mais comuns são os tipos de coleção
List<T> e Map<K,V>.
Por exemplo, no código a seguir a função printInts() imprime uma lista de inteiros,
e main() cria uma lista e a passa para printInts().
void printInts(List<int> a) => print(a);
void main() {
final list = [];
list.add(1);
list.add('2');
printInts(list);
}
O código anterior resulta em um erro de tipo em list (destacado
acima) na chamada de printInts(list):
error - The argument type 'List<dynamic>' can't be assigned to the parameter type 'List<int>'. - argument_type_not_assignable
O erro destaca uma conversão implícita não sound de List<dynamic> para List<int>.
A variável list tem o tipo estático List<dynamic>. Isso acontece porque a
declaração de inicialização var list = [] não fornece ao analisador
informação suficiente para inferir um argumento de tipo mais específico que dynamic.
A função printInts() espera um parâmetro do tipo List<int>,
o que causa uma incompatibilidade de tipos.
Ao adicionar uma anotação de tipo (<int>) na criação da lista
(destacada abaixo), o analisador reclama que
um argumento string não pode ser atribuído a um parâmetro int.
Remover as aspas em list.add('2') resulta em código
que passa na análise estática e roda sem erros ou avisos.
void printInts(List<int> a) => print(a);
void main() {
final list = <int>[];
list.add(1);
list.add(2);
printInts(list);
}
O que é soundness?
#
Soundness trata de garantir que seu programa não consiga chegar a certos estados
inválidos. Um sistema de tipos sound significa que você nunca pode chegar a um
estado em que uma expressão avalia para um valor que não corresponde ao
tipo estático da expressão. Por exemplo, se o tipo estático de uma expressão for
String, em tempo de execução você tem a garantia de obter apenas uma string
quando avaliá-la.
O sistema de tipos do Dart, assim como os sistemas de tipos de Java e C#, é sound. Ele
garante essa soundness usando uma combinação de verificação estática
(erros em tempo de compilação) e verificações em tempo de execução. Por exemplo, atribuir uma
String
a um int é um erro em tempo de compilação. Converter um objeto para
String
usando as String falha com um erro em tempo de execução se o objeto não for uma
String.
Os benefícios da soundness
#Um sistema de tipos sound tem vários benefícios:
-
Revelar bugs relacionados a tipos em tempo de compilação.
Um sistema de tipos sound força o código a ser inequívoco sobre seus tipos, então bugs relacionados a tipos que seriam difíceis de encontrar em tempo de execução são revelados em tempo de compilação. -
Código mais legível.
O código é mais fácil de ler porque você pode confiar que um valor realmente tem o tipo especificado. Em Dart sound, os tipos não mentem. -
Código mais fácil de manter.
Com um sistema de tipos sound, quando você muda um trecho de código, o sistema de tipos pode avisar sobre os outros trechos de código que acabaram de quebrar. -
Melhor compilação antecipada (AOT).
Embora a compilação AOT seja possível sem tipos, o código gerado é bem menos eficiente.
Dicas para passar na análise estática
#A maioria das regras para tipos estáticos é fácil de entender. Aqui estão algumas das regras menos óbvias:
- Use tipos de retorno sound ao sobrescrever métodos.
- Use tipos de parâmetro sound ao sobrescrever métodos.
- Não use uma lista dinâmica como se fosse uma lista tipada.
Vamos ver essas regras em detalhe, com exemplos que usam a seguinte hierarquia de tipos:
Use tipos de retorno sound ao sobrescrever métodos
#
O tipo de retorno de um método em uma subclasse deve ser o mesmo tipo ou um
subtipo do tipo de retorno do método na superclasse.
Considere o método getter na classe Animal:
class Animal {
void chase(Animal a) {
...
}
Animal get parent => ...
}
O método getter parent retorna um Animal. Na subclasse HoneyBadger,
você pode substituir o tipo de retorno do getter por HoneyBadger
(ou qualquer outro subtipo de Animal), mas um tipo não relacionado não é permitido.
class HoneyBadger extends Animal {
@override
void chase(Animal a) {
...
}
@override
HoneyBadger get parent => ...
}
class HoneyBadger extends Animal {
@override
void chase(Animal a) {
...
}
@override
Root get parent => ...
}
Use tipos de parâmetro sound ao sobrescrever métodos
#O parâmetro de um método sobrescrito deve ter o mesmo tipo ou um supertipo do parâmetro correspondente na superclasse. Não "restrinja" o tipo do parâmetro substituindo-o por um subtipo do parâmetro original.
Considere o método chase(Animal) da classe Animal:
class Animal {
void chase(Animal a) {
...
}
Animal get parent => ...
}
O método chase() recebe um Animal. Um HoneyBadger persegue qualquer coisa.
É aceitável sobrescrever o método chase() para receber qualquer coisa (Object).
class HoneyBadger extends Animal {
@override
void chase(Object a) {
...
}
@override
Animal get parent => ...
}
O código a seguir restringe o parâmetro do método chase()
de Animal para Mouse, uma subclasse de Animal.
class Mouse extends Animal {
...
}
class Cat extends Animal {
@override
void chase(Mouse a) {
...
}
}
Esse código não é type safe, porque seria possível definir um gato e mandá-lo atrás de um crocodilo:
Animal a = Cat();
a.chase(Alligator()); // Not type safe or feline safe.
Não use uma lista dinâmica como lista tipada
#
Uma lista dynamic é útil quando você quer uma lista com
coisas de tipos diferentes. No entanto, você não pode usar uma
lista dynamic como se fosse uma lista tipada.
Essa regra também se aplica a instâncias de tipos genéricos.
O código a seguir cria uma lista dynamic de Dog e a atribui a
uma lista do tipo Cat, o que gera um erro durante a análise estática.
void main() {
List<Cat> foo = <dynamic>[Dog()]; // Error
List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}
Verificações em tempo de execução
#Verificações em tempo de execução lidam com problemas de type safety que não podem ser detectados em tempo de compilação.
Por exemplo, o código a seguir lança uma exceção em tempo de execução porque é um erro converter uma lista de cachorros em uma lista de gatos:
void main() {
List<Animal> animals = <Dog>[Dog()];
List<Cat> cats = animals as List<Cat>;
}
Downcasts implícitos a partir de dynamic
#
Expressões com tipo estático dynamic podem ser
convertidas implicitamente para um tipo mais específico.
Se o tipo real não corresponder, a conversão lança um erro em tempo de execução.
Considere o método assumeString a seguir:
int assumeString(dynamic object) {
String string = object; // Check at run time that `object` is a `String`.
return string.length;
}
Neste exemplo, se object for uma String, a conversão funciona.
Se não for um subtipo de String, como um int,
um TypeError é lançado:
final length = assumeString(1);
Inferência de tipos
#
O analisador consegue inferir tipos de campos, métodos, variáveis locais
e a maioria dos argumentos de tipo genéricos.
Quando o analisador não tem informação suficiente para inferir
um tipo específico, ele usa o tipo dynamic.
Aqui está um exemplo de como a inferência de tipos funciona com genéricos.
Neste exemplo, uma variável chamada arguments guarda um map que
associa chaves string a valores de vários tipos.
Se você tipar a variável explicitamente, pode escrever isto:
Map<String, Object?> arguments = {'argA': 'hello', 'argB': 42};
Alternativamente, você pode usar var ou final e deixar o Dart inferir o tipo:
var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>
O literal de map infere seu tipo a partir das entradas,
e então a variável infere seu tipo a partir do tipo do literal de map.
Neste map, as chaves são ambas strings, mas os valores têm
tipos diferentes (String e int, cujo limite superior é
Object).
Então o literal de map tem o tipo Map<String, Object>,
e a variável arguments também.
Inferência de campos e métodos
#Um campo ou método sem tipo especificado que sobrescreve um campo ou método da superclasse herda o tipo do método ou campo da superclasse.
Um campo que não tem tipo declarado nem herdado, mas que é declarado com um valor inicial, recebe um tipo inferido com base nesse valor inicial.
Inferência de campos estáticos
#Campos e variáveis estáticos recebem seus tipos inferidos a partir do inicializador. Observe que a inferência falha se encontrar um ciclo (ou seja, inferir o tipo da variável depende de conhecer o tipo dessa própria variável).
Inferência de variáveis locais
#Os tipos de variáveis locais são inferidos a partir do inicializador, se houver. Atribuições posteriores não são consideradas. Isso pode fazer com que um tipo preciso demais seja inferido. Se for o caso, você pode adicionar uma anotação de tipo.
var x = 3; // x is inferred as an int.
x = 4.0;
num y = 3; // A num can be double or int.
y = 4.0;
Inferência de argumentos de tipo
#Os argumentos de tipo em chamadas de construtores e em invocações de métodos genéricos são inferidos com base na combinação de informação descendente, vinda do contexto de ocorrência, e informação ascendente, vinda dos argumentos do construtor ou do método genérico. Se a inferência não estiver fazendo o que você quer ou espera, você sempre pode especificar explicitamente os argumentos de tipo.
// Inferred as if you wrote <int>[].
List<int> listOfInt = [];
// Inferred as if you wrote <double>[3.0].
var listOfDouble = [3.0];
// Inferred as Iterable<int>.
var ints = listOfDouble.map((x) => x.toInt());
No último exemplo, x é inferido como double usando informação descendente.
O tipo de retorno da closure é inferido como int usando informação ascendente.
O Dart usa esse tipo de retorno como informação ascendente ao inferir o argumento de tipo
do método map(): <int>.
Inferência usando limites
#Com o recurso de inferência usando limites, o algoritmo de inferência de tipos do Dart gera restrições combinando as restrições existentes com os limites de tipo declarados, não apenas aproximações do tipo "melhor esforço".
Isso é especialmente importante para tipos F-bounded,
em que a inferência usando limites infere corretamente que, no exemplo abaixo,
X pode ser vinculado a B.
Sem o recurso, o argumento de tipo precisa ser especificado explicitamente: f<B>(C()):
class A<X extends A<X>> {}
class B extends A<B> {}
class C extends B {}
void f<X extends A<X>>(X x) {}
void main() {
f(B()); // OK.
// OK. Without using bounds, inference relying on best-effort approximations
// would fail after detecting that `C` is not a subtype of `A<C>`.
f(C());
f<B>(C()); // OK.
}
Aqui está um exemplo mais realista, usando tipos do dia a dia no Dart como int ou num:
X max<X extends Comparable<X>>(X x1, X x2) => x1.compareTo(x2) > 0 ? x1 : x2;
void main() {
// Inferred as `max<num>(3, 7)` with the feature, fails without it.
max(3, 7);
}
Com a inferência usando limites, o Dart consegue desconstruir argumentos de tipo,
extraindo informação de tipo do limite de um parâmetro de tipo genérico.
Isso permite que funções como f no exemplo a seguir preservem tanto
o tipo específico do iterável (List ou Set) quanto
o tipo dos elementos.
Antes da inferência usando limites, isso não era possível
sem perder type safety ou a informação de tipo específica.
(X, Y) f<X extends Iterable<Y>, Y>(X x) => (x, x.first);
void main() {
var (myList, myInt) = f([1]);
myInt.whatever; // Compile-time error, `myInt` has type `int`.
var (mySet, myString) = f({'Hello!'});
mySet.union({}); // Works, `mySet` has type `Set<String>`.
}
Sem a inferência usando limites, myInt teria o tipo dynamic.
O algoritmo anterior não detectaria a expressão incorreta
myInt.whatever em tempo de compilação, e lançaria um erro em tempo de execução.
Por outro lado, mySet.union({}) seria um erro em tempo de compilação
sem a inferência usando limites, porque o algoritmo anterior não conseguia
preservar a informação de que mySet é um Set.
Para mais informações sobre o algoritmo de inferência usando limites, leia o documento de design.
Substituindo tipos
#Ao sobrescrever um método, você está substituindo algo de um tipo (no método antigo) por algo que pode ter um novo tipo (no novo método). De forma semelhante, ao passar um argumento para uma função, você está substituindo algo que tem um tipo (um parâmetro com tipo declarado) por algo que tem outro tipo (o argumento real). Quando você pode substituir algo que tem um tipo por algo que tem um subtipo ou um supertipo?
Ao substituir tipos, ajuda pensar em termos de consumidores e produtores. Um consumidor absorve um tipo, e um produtor gera um tipo.
Você pode substituir o tipo de um consumidor por um supertipo, e o tipo de um produtor por um subtipo.
Vejamos exemplos de atribuição de tipo simples e de atribuição com tipos genéricos.
Atribuição de tipo simples
#Ao atribuir objetos a objetos, quando você pode substituir um tipo por um tipo diferente? A resposta depende de o objeto ser um consumidor ou um produtor.
Considere a seguinte hierarquia de tipos:
Considere a seguinte atribuição simples, em que Cat c é um consumidor
e Cat() é um produtor:
Cat c = Cat();
Em uma posição de consumo, é seguro substituir algo que consome um
tipo específico (Cat) por algo que consome qualquer coisa (Animal),
então substituir Cat c por Animal c é permitido, porque
Animal é
um supertipo de Cat.
Animal c = Cat();
Mas substituir Cat c por MaineCoon c quebra a type safety,
porque a superclasse pode fornecer um tipo Cat com comportamentos diferentes,
como Lion:
MaineCoon c = Cat();
Em uma posição de produção, é seguro substituir algo que produz um
tipo (Cat) por um tipo mais específico (MaineCoon). Portanto, o
seguinte é permitido:
Cat c = MaineCoon();
Atribuição de tipo genérico
#
As regras são as mesmas para tipos genéricos? Sim. Considere a hierarquia
de listas de animais: uma List de Cat é um subtipo de uma
List de
Animal, e um supertipo de uma List de MaineCoon:
No exemplo a seguir,
você pode atribuir uma lista de MaineCoon a myCats,
porque List<MaineCoon> é um subtipo de List<Cat>:
List<MaineCoon> myMaineCoons = ...
List<Cat> myCats = myMaineCoons;
E na direção contrária?
Você pode atribuir uma lista de Animal a uma List<Cat>?
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals;
Essa atribuição não passa na análise estática,
porque cria uma conversão implícita para baixo (downcast), que é proibida a partir de tipos não-dynamic, como
Animal.
Para que esse tipo de código passe na análise estática, você pode usar um cast explícito.
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals as List<Cat>;
Um cast explícito ainda pode falhar em tempo de execução,
dependendo do tipo real da lista que está sendo convertida (myAnimals).
Métodos
#Ao sobrescrever um método, as regras de produtor e consumidor continuam valendo. Por exemplo:
Para um consumidor (como o método chase(Animal)), você pode substituir
o tipo do parâmetro por um supertipo. Para um produtor (como
o método getter parent), você pode substituir o tipo de retorno por
um subtipo.
Para mais informações, veja Use tipos de retorno sound ao sobrescrever métodos e Use tipos de parâmetro sound ao sobrescrever métodos.
Parâmetros covariant
#
Alguns padrões de código (raramente usados) dependem de restringir um tipo
sobrescrevendo o tipo de um parâmetro com um subtipo, o que é inválido.
Nesse caso, você pode usar a keyword covariant para
informar ao analisador que você está fazendo isso intencionalmente.
Isso remove o erro estático e, em vez disso, verifica um tipo de argumento
inválido em tempo de execução.
A seguir, um exemplo de como você pode usar covariant:
class Animal {
void chase(Animal x) {
...
}
}
class Mouse extends Animal {
...
}
class Cat extends Animal {
@override
void chase(covariant Mouse x) {
...
}
}
Embora este exemplo mostre o uso de covariant na subclasse,
a keyword covariant pode ser colocada tanto no método da superclasse
quanto no da subclasse.
Normalmente, o método da superclasse é o melhor lugar para ela.
A keyword covariant se aplica a um único parâmetro e também
é suportada em setters e campos.
Outros recursos
#Os recursos a seguir têm mais informações sobre Dart sound:
- Corrigindo falhas de promoção de tipo - Entenda e aprenda a corrigir erros de promoção de tipo.
- Null safety sound - Saiba mais sobre como escrever código com null safety sound.
- Personalizando a análise estática - Como configurar e personalizar o analisador e o linter usando um arquivo de opções de análise.
Salvo indicação em contrário, a documentação neste site reflete Dart 3.13.7. Página atualizada pela última vez em 2026-11-12. Ver código-fonte ou relatar um problema.
Encontrou essa página sem tradução ou que precisa de correção? Abra uma issue e nos ajude a manter esse site em PT-BR para ajudar pessoas como você.