INTRODUÇÃO
Object Calisthenics me fizeram lembrar de vários conceitos de Orientação a Objetos e eu gostaria de aplicar os mesmos raciocínios em typescript. Não apenas portar o código para outra sintaxe, mas conseguir traduzir as necessidades e a estratégia para outro modelo de tipagem.
O terceiro exercício de calistenia é bem frutífero para análise da linguagem porque expõe restrições e recursos que divergem em fundamento da abordagem clássica. Uma definição possível, por William Durand:
3. Wrap All Primitives And Strings
Following this rule is pretty easy, you simply have to encapsulate all the primitives within objects, in order to avoid the Primitive Obsession anti-pattern.
If the variable of your primitive type has a behavior, you MUST encapsulate it.
Após o uso desta técnica continua existindo a mesma quantidade de primatas, então nem é um problemas com eles em si. Mas todas suas interações passam a ser tuteladas por um objeto maior de idade com carteira de habilitação. Um primata solto pode estar fazendo qualquer coisa - o subtítulo do produto ou não fazendo nada.
E quando ele é passado pra frente acontece uma completa amnésia de seus objetivos, preferências e sonhos, não dá mais pra discernir sua identidade no meio do rebanho, enquanto que objetos são alocados na heap e podem reter traços de história.
const ape1 = 'xita'
ape1.dream = 'banana'
const ape2 = () => 'xita'
ape2.dream = 'banana'
const showDreams = x => console.log(`My dream is ${x.dream}`)
showDreams(ape1) // My dream is undefined
showDreams(ape2) // My dream is banana
Mesmo dois objetos criados iguais possuem referências diferentes e são tratados como indivíduos.
Tipos Marcados
Vestimos eles com nossos signos de civilização para envergonhar a natureza latente que espreita nossa própria falta de critério e excesso de controle.
O que antes era produzido, regulado e consumido localmente, de forma quase banal, passa a ser governado externamente e ganha economia de escala. Isso em nome da produtividade e alto padrão de qualidade. Podemos defender o is-odd se atravessar o perímetro urbano para outro pacote for disponível. Ou até mesmo uma gentrificação continental se pudermos emigrar pra outra linguagem, segundo Greenspun's Tenth Rule:
Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp
Mas vou traçar a linha do exagero antes mesmo da classe.
Symbol em typescript é um tipo primitivo não enumerável, significa que você não o enxerga se for usado como nome de atributo. E para conseguir um novo você precisa chama uma função própria, e ela nunca os repetem.
const k = Symbol();
export const myBag = {
[k]: 'serial number 123456'
}
export const getSerialNumber = x => x[k]
Apenas getSerialNumber
tem acesso ao símbolo de k
pra conseguir ler o atributo privado. Mas ainda não é possível marcar outros tipos primitivos em tempo de execução com esses símbolos, como adicionar este atributo a uma string? Podemos fazer isso em tempo de desenvolvimento na tipagem:
declare const brand: unique symbol;
export type Brand<T, TAG> = T & { [brand]: TAG }
A intersecção A & B
é o subconjunto que satisfaz simultaneamente A
e B
. Em tempo de execução B
como objeto praticamente vazio não macula a desenvoltura de A
, mas o compilador vai conseguir distribuir e diferenciar Brand<string, 'CPF'>
de Brand<string, 'EMAIL'>
.
Esta diferenciação não acontece com type Email = string
! A tag só existe no mundo dos tipos, e mesmo se sobrasse após a compilação nem seria acionável porque estaria atrás de um Symbol.
Para centralizar o policiamento podemos criar uma função responsável pela manufatura dos tipos Email
adequadamente:
export const assertEmail = (email: string) => {
const isEmail = true // (...)
if (isEmail) {
return email as Email
}
throw new Error(`${email} is not an email`)
}
Você tem assegurado em tempo de execução que são manufaturados corretamente sem precisar trocar sua essência de string. E durante o desenvolvimento a tipagem coordena toda a cadeia de procedimento aconselhando o algoritmo.
Todo Email
é uma string
, mas nem toda string
é um Email
. Todo lugar que aceitava uma string
vai aceitar Email
, mas nenhum lugar que aceita Email
vai tolerar uma string
.
A calistenia é um tipo de treinamento que utiliza o peso do próprio corpo. Mas enjaular primatas em classes significa que toda interação será mediada e o código vai precisar de refatoração. Além do custo da alocação de memória ainda exige construção de consenso, o que pode desgasta e distrair a arquitetura. Em typescript dá para taguear com símbolos e receber suporte emocional do seu editor, sem necessidade de aparelhos complexos para controlar a execução.
FAZ MEU TIPO O SUFICIENTE
A maioria das linguagens canônicas de Orientação a Objetos possuem tipagem nominal, significa que mesmo que dois objetos possuam os mesmos atributos mas foram instanciados de classes com nomes diferentes eles não são intercambiáveis. Já em typescript se tiver olhos de macaco 🙈, orelha de macaco 🙉, boca de macaco 🙊, rabo de macaco 🐒, então é um macaco 🐵. E se tiver bico de pato 🦆, orelha de porco 🐷, chifre de vaca 🐄 e só precisar de 2 olhos, serve também.
A intersecção A & B
poderia ser esquematizada com interfaces assim:
interface A {
ka1: string;
ka2: string;
}
interface B {
kb1: string;
kb2: string;
}
interface A_AND_B {
ka1: string;
ka2: string;
kb1: string;
kb2: string;
}
Se uma função necessitar parcialmente de um tipo entregue também está adequado, não precisa ser estritamente igual, basta ser suficientemente boa:
const takesA = (arg: A) => {
//
}
const takesB = (arg: B) => {
//
}
const givesAB = () => ({
ka1: 'va1',
ka2: 'va2',
kb1: 'vb1',
kb2: 'vb2',
})
takesA(givesAB())
takesB(givesAB())
Repare que a função givesAB
nem retorna um objeto do tipo A_AND_B
, é de uma interface anônima mas que possui todos os atributos obrigatórios para cada uma das takes. E internamente as funções takes não sabem da existência de nada além do que está declarado na sua interface.
Também se aplica a uma coleção de tipos interseccionados
declare const brandEyes: unique symbol;
type BrandEyes<T, TBrand> = T & { [brandEyes]: TBrand }
declare const brandEars: unique symbol;
type BrandEars<T, TBrand> = T & { [brandEars]: TBrand }
declare const brandMouth: unique symbol;
type BrandMouth<T, TBrand> = T & { [brandMouth]: TBrand }
declare const brandTail: unique symbol;
type BrandTail<T, TBrand> = T & { [brandTail]: TBrand }
type EyesMacaco<T> = BrandEyes<T, 'MACACO'>
type EarsMacaco<T> = BrandEars<T, 'MACACO'>
type MouthMacaco<T> = BrandMouth<T, 'MACACO'>
type TailMacaco<T> = BrandTail<T, 'MACACO'>
// coleção de tipos interseccionados
type Ape = TailMacaco<MouthMacaco<EarsMacaco<EyesMacaco<string>>>>
const macaco = 'xita' as Ape
const apetician = (ape: EyesMacaco<string>) => {
// does exams
}
apetician(macaco)
apetician
requisita como argumento apenas olhos de macaco, mas se você entregar o macaco inteiro está satisfeito o mínimo exigido.
Esse na foto é Liskov percebendo que não precisa de uma intrincada correlação de herança pra fazer substituições. Todos os parâmetro são como interfaces abstratas porque você não tem restrição nominal. Não é necessário criar camadas de indireção para contornar condições que nunca foram estabelecidas.
Conclusão
Tipagem estrutural é bem flexível, ainda mais contando com o turing-completo do typescript. É possível adicionar uma pitada de tempero nominal pra conseguir uma rastreabilidade mais precisa e com isto não deixar os primatas tão precarizados. Um uso comum desta técnica de branding é quando precisa classificar ids ou hashes que seriam indistinguíveis.
Aqui algumas sugestões de leitura:
Top comments (1)
👏👏👏