TypeScript pro Swift: o que o compilador te cobra e o TS deixa passar
Tudo que você sabe sobre null está errado
No TypeScript, null e undefined são valores. Você escreve let name: string | null e a string pode estar ali ou pode ser null. Simples.
No Swift, null nem existe. Tem nil. Só que a ideia é diferente: quando você declara String?, o tipo da variável é Optional<String>. E Optional é um enum com dois casos: existe um valor ou não existe.
// Swift 5.10
let name: String? = "Luan"
let empty: String? = nil
// TypeScript 5.4
const name: string | null = "Luan";
const empty: string | null = null;
Parece igual no começo. Mas não é.
No TypeScript, null faz parte dos valores que aquela variável pode receber. No Swift, você tá trabalhando com Optional<String>. Pra usar a String, precisa tirar o valor de dentro do Optional.
Foi aí que comecei a entender por que Swift me obrigava a tratar tanta coisa que eu simplesmente deixava passar no TypeScript.
Unwrap: abrindo o Optional
Quando você usa name no TypeScript, você tem uma string ou null. No Swift, se name é String?, você tem um Optional<String>. Pra chegar na string de verdade, precisa fazer unwrap.
Tem algumas formas:
// Force unwrap: perigoso
let forced = name!
// Optional binding: seguro
if let unwrapped = name {
print(unwrapped)
}
// Nil coalescing: fallback
let safe = name ?? "desconhecido"
// Optional chaining: propaga o nil
let upper = name?.uppercased()
No TypeScript, eu pensaria mais ou menos assim:
// Non-null assertion: perigoso
let forced = name!;
// Check
if (name !== null) {
console.log(name);
}
// Fallback
let safe = name ?? "desconhecido";
// Optional chaining
let upper = name?.toUpperCase();
No TypeScript, name?.toUpperCase() retorna string | undefined. Se name for null, a expressão vira undefined, mas name continua sendo null.
No Swift, name?.uppercased() retorna String?. Se name for nil, o resultado também é nil. O name já era Optional<String>.none.
No começo isso parece detalhe de linguagem. Eu também tratei assim.
Só que começa a importar quando você modela dados. "" e nil não significam a mesma coisa. No TypeScript, a equipe precisa decidir como vai tratar "", null e undefined. No Swift, String e String? já são tipos diferentes, então o compilador entra nessa conversa também.
O momento em que Optional fez sentido pra mim
Eu estava migrando uma tela de listagem que vinha de um backend em TypeScript. O JSON tinha campos opcionais. No TS eu tinha isso:
interface Account {
id: string;
holderName: string | null;
balance: number;
nickname?: string;
}
No Swift, minha primeira versão ficou assim:
struct Account: Codable {
let id: String
let holderName: String?
let balance: Double
let nickname: String?
}
Funciona. Decodifica o JSON.
Mas tem um detalhe: holderName e nickname viraram String?. No TypeScript eu conseguia representar null, quando o campo veio nulo, e undefined, quando o campo nem veio no JSON.
No Swift, os dois acabam como nil. Na maioria dos casos, tá tudo bem.
Agora, se eu realmente precisar saber a diferença entre “tem valor”, “veio null” e “não veio”, aí preciso modelar isso com um enum próprio. Existe discussão sobre isso desde Swift 4.x e o Swift 5.10 ainda não traz uma solução nativa elegante pra esse caso.
Depois disso parei de olhar Optional como uma anotação em cima da variável. Passei a pensar nele como o tipo que eu realmente tenho na mão.
Value types vs Reference types: struct e class
No TypeScript, objetos e arrays são reference types. Primitives como string, number e boolean são passados por valor. No dia a dia, eu quase nunca pensava nisso ao criar uma interface.
No Swift eu precisei pensar.
// Value type: cópia ao atribuir
struct Point {
var x: Double
var y: Double
}
var a = Point(x: 0, y: 0)
var b = a
b.x = 10
// a.x continua 0.
// b é uma cópia independente.
Com class muda:
class Box {
var value: Int
init(value: Int) {
self.value = value
}
}
let c = Box(value: 0)
let d = c
d.value = 10
// c.value agora é 10.
// c e d apontam pro mesmo objeto.
No TypeScript:
const a = { x: 0, y: 0 };
const b = a;
b.x = 10;
// a.x agora é 10.
// Mesma referência.
TypeScript não tem struct. Também não tem value type customizado. Se eu quiser copiar um objeto, posso usar spread ({ ...obj }) ou structuredClone. No Swift, uma struct tem semântica de valor em atribuição, passagem de argumento e retorno de função.
E isso aparece em lugares que eu não esperava.
Com estado compartilhado entre threads, por exemplo, classes podem colocar duas partes do código olhando pra mesma instância mutável. Com value types, cada contexto trabalha com seu próprio valor. Em Swift 5.10, usando Swift Concurrency, esse tipo de decisão fica ainda mais visível. actor entra justamente quando você precisa proteger estado mutável compartilhado.
Quem vem de TypeScript costuma chegar com a cabeça de objeto mutável e referência compartilhada. Eu cheguei assim.
Hoje começo com struct. Se preciso de identidade compartilhada, herança ou outra característica de referência, aí considero class.
Na minha primeira arquitetura no AppFlowBank eu tinha colocado class em tudo. Funcionava, mas o estado vazava entre casos de teste. Quando mudei esses modelos pra struct, os testes ficaram isolados sem eu precisar controlar o mesmo estado compartilhado.
let vs var: aqui a memória muscular atrapalha
No TypeScript:
const x = 10; // não pode reatribuir
let y = 10; // pode reatribuir
No Swift:
let x = 10 // imutável
var y = 10 // mutável
A ordem troca. let é constante e var é variável. Depois acostuma.
O que me pegou mesmo foi let com struct. No Swift, se uma struct está em uma constante, você não sai alterando os campos dela. No TypeScript, const obj = { x: 1 } impede trocar obj por outro objeto, mas obj.x = 5 continua válido.
struct Size {
var width: Double
var height: Double
}
let fixed = Size(width: 100, height: 200)
// fixed.width = 50
// erro de compilação
var flexible = Size(width: 100, height: 200)
flexible.width = 50 // ok
Com class é diferente:
class Counter {
var count = 0
}
let counter = Counter()
counter.count = 5 // ok
// counter = Counter()
// erro: não pode trocar a referência
Então eu não tento traduzir let mentalmente pra const. Primeiro olho se estou lidando com value type ou reference type, porque isso muda o que pode ser alterado.
Optional chaining em profundidade
O ?. existe nas duas linguagens. A sintaxe engana um pouco porque parece que o comportamento vai ser idêntico.
let user: User? = getUser()
let city = user?.address?.city
// city é String?
Cada ?. continua a cadeia enquanto existe valor. Se algum ponto for nil, o resultado é nil. E o tipo final acompanha isso.
No TypeScript:
const user: User | null = getUser();
const city = user?.address?.city;
// city é string | undefined
No Swift, city é String?. É Optional<String>.
Tem outra coisa que me pegou: Optional pode conter outro Optional.
let nested: String?? = .some(.some("oi"))
String?? é Optional<Optional<String>>. Dá pra aninhar mais, embora quase ninguém queira fazer isso na prática. Isso existe porque Optional é um enum genérico e pode ser aninhado como qualquer outro tipo.
No TypeScript, string | null | null é simplificado pra string | null. No Swift, String?? e String? continuam sendo tipos diferentes. Um tem quatro estados possíveis. O outro tem dois.
Tratando erro
No TypeScript, o caminho comum é try/catch:
try {
const data = JSON.parse(input);
} catch (e) {
console.error(e);
}
No Swift, uma função que pode lançar erro usa throws, e a chamada usa try:
do {
let data = try JSONDecoder().decode(User.self, from: input)
} catch {
print(error)
}
E tem try? e try!:
let data = try? JSONDecoder().decode(User.self, from: input)
// data é User?. Se der erro, vira nil.
let forced = try! JSONDecoder().decode(User.self, from: input)
// forced é User. Se der erro, crash.
Eu gosto de pensar no try? como um ponto de encontro entre os dois sistemas. O erro deixa de ser propagado e o resultado passa a ser opcional. No TypeScript não tem um equivalente direto. O mais próximo seria criar um wrapper que captura o erro e retorna null, como bibliotecas tipo neverthrow fazem na mão.
As coisas que o compilador faz por você
Aqui foi onde mais senti diferença saindo do TypeScript.
Se eu tenho isso:
let name: String? = "Luan"
let greeting = "Olá, " + name
// erro: não concatena String com Optional
Eu não consigo fingir que o Optional não existe. Preciso tratar:
let greeting = "Olá, " + (name ?? "estranho")
No começo é meio chato. Você escreve mais e precisa parar pra pensar no tipo.
Depois vira hábito.
Quando volto pro TypeScript, sinto falta desse atrito em alguns pontos. No Swift, se uma coisa pode não existir, isso aparece no tipo e vai me acompanhar até eu decidir o que fazer com ela.
O que eu levaria desse texto
Se você vem do TypeScript, eu prestaria atenção em três coisas logo no começo.
Optionalé um tipo. Você precisa lidar com ele antes de usar o valor que pode estar lá dentro.structtem semântica de valor. Atribuir umastructa outra variável não cria a mesma relação de referência que você espera de um objeto em TypeScript.- E
letmuda de comportamento prático dependendo de você estar trabalhando com value type ou reference type.
O resto vem fazendo projeto, errando e lendo erro de compilação. Essas três coisas foram as que mais fizeram meu código Swift parecer TypeScript traduzido quando eu ainda não tinha entendido direito como a linguagem queria ser usada.