A história de como passar dois dias reescrevendo o mesmo código várias vezes.

Entrada
Neste artigo, omitirei detalhes sobre Hapi, Joi, roteamento e validate: { payload: ... }
, o que implica que você já entende do que se trata, bem como a terminologia, como "interfaces", "tipos" e similares . Vou falar apenas sobre uma estratégia baseada em turnos, e não a mais bem-sucedida, sobre meu treinamento nessas coisas.
Um pouco de fundo
Agora eu sou o único desenvolvedor de back-end (a saber, escrever código) no projeto. A funcionalidade não é a essência, mas a essência principal é um perfil bastante longo com dados pessoais. A velocidade e a qualidade do código são baseadas na minha pouca experiência em trabalhar de forma independente em projetos do zero, ainda menos experiência em trabalhar com JS (apenas no 4º mês) e, ao longo do caminho, de maneira muito oblíqua, escrevo em TypeScript (a seguir - TS). As datas são compactadas, os rolos são compactados, as edições chegam constantemente e acaba por escrever primeiro o código da lógica de negócios e depois as interfaces no topo. No entanto, um dever técnico é capaz de recuperar o atraso e bater na tampa, o que, aproximadamente, aconteceu conosco.
Após 3 meses de trabalho no projeto, finalmente concordei com meus colegas de mudar para um único dicionário para que as propriedades do objeto sejam nomeadas e escritas da mesma maneira em todos os lugares. Sob esse ramo, é claro, eu me comprometi a escrever uma interface e fiquei preso a ela por dois dias úteis.
O problema
Um perfil de usuário simples será um exemplo abstrato.
Primeiro A etapa zero de um bom desenvolvedor: descrever dados escrever testes;- Primeiro passo:
escrever testes Descrever dados - e assim por diante.
Suponha que os testes já tenham sido escritos para esse código, resta descrever os dados:
interface IUser { name: string; age: number; phone: string | number; } const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' };
Bem, aqui tudo é claro e extremamente simples. Todo esse código, como lembramos, no back-end, ou melhor, na API, ou seja, o usuário é criado com base nos dados provenientes da rede. Portanto, precisamos validar os dados recebidos e ajudar a Joi nisso:
const joiUserValidator = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
A solução "na testa" está pronta. O menos óbvio dessa abordagem é que o validador é completamente divorciado da interface. Se durante a vida útil do aplicativo os campos forem alterados / adicionados ou seu tipo for alterado, essa alteração precisará ser rastreada manualmente e indicada no validador. Eu acho que não haverá desenvolvedores responsáveis até que algo caia. Além disso, em nosso projeto, o questionário consiste em mais de 50 campos em três níveis de aninhamento e é extremamente difícil entender isso, mesmo sabendo tudo de cor.
Simplesmente não podemos especificar const joiUserValidator: IUser
, porque o Joi
usa seus próprios tipos de dados, o que gera erros ao compilar o tipo Type 'NumberSchema' is not assignable to type 'number'
. Mas deve haver uma maneira de realizar a validação na interface?

Talvez eu não tenha pesquisado no google corretamente ou estudado mal as respostas, mas todas as decisões se extractTypes
em extractTypes
tipos e algum tipo de bicicleta feroz, como esta :
type ValidatedValueType<T extends joi.Schema> = T extends joi.StringSchema ? string : T extends joi.NumberSchema ? number : T extends joi.BooleanSchema ? boolean : T extends joi.ObjectSchema ? ValidatedObjectType<T> : never;
Solução
Use bibliotecas de terceiros
Porque não Quando perguntei às pessoas sobre minha tarefa, recebi em uma das respostas e, posteriormente, e aqui, nos comentários (graças ao keenondrums ), links para essas bibliotecas:
https://github.com/typestack/class-validator
https://github.com/typestack/class-transformer
No entanto, havia um interesse em descobrir você mesmo, entender melhor o trabalho da TS, e nada era necessário para resolver o problema imediatamente.
Obter todas as propriedades
Como não tinha trabalho anterior com estática, o código acima descobriu a América em termos de uso de operadores ternários em tipos. Felizmente, não foi possível aplicá-lo no projeto. Mas eu encontrei outra bicicleta interessante:
interface IUser { name: string; age: number; phone: string | number; } type UserKeys<T> = { [key in keyof T]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
TypeScript
condições bastante complicadas e misteriosas, permite obter, por exemplo, chaves da interface, como se fosse um objeto JS normal, no entanto, apenas na construção de type
e na key in keyof T
e somente através de genéricos. Como resultado do tipo UserKeys
, todos os objetos que implementam as interfaces devem ter o mesmo conjunto de propriedades, mas os tipos de valores podem ser arbitrários. Isso inclui dicas no IDE, mas ainda não fornece uma indicação clara dos tipos de valores.
Aqui está outro caso interessante que eu não poderia usar. Talvez você possa me dizer por que isso é necessário (embora eu suponha parcialmente, não há exemplo aplicado suficiente):
interface IUser { name: string; age: number; phone: string | number; } interface IUserJoi { name: Joi.StringSchema, age: Joi.NumberSchema, phone: Joi.AlternativesSchema } type UserKeys<T> = { [key in keyof T]: T[key]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const userJoiValidator: UserKeys<IUserJoi> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
Use tipos de variáveis
Você pode definir explicitamente os tipos e, usando "OU" e extrair as propriedades, obtenha um código de trabalho local:
type TString = string | Joi.StringSchema; type TNumber = number | Joi.NumberSchema; type TStdAlter = TString | TNumber; type TAlter = TStdAlter | Joi.AlternativesSchema; export interface IUser { name: TString; age: TNumber; phone: TAlter; } type UserKeys<T> = { [key in keyof T]; } const olex: UserKeys<IUser> = { name: 'Olex', age: 67, phone: '79998887766' }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
O problema desse código se manifesta quando queremos pegar um objeto válido, por exemplo, no banco de dados, ou seja, o TS não sabe de antemão que tipo de dados serão - simples ou Joi. Isso pode causar um erro ao tentar executar operações matemáticas em um campo que é esperado como number
:
const someUser: IUser = getUserFromDB({ name: 'Aleg' }); const someWeirdMath = someUser.age % 10;
Este erro vem do Joi.NumberSchema
porque a idade pode não ser apenas o number
. Pelo que eles lutaram e encontraram.
Combine duas soluções em uma?
Em algum momento, o dia de trabalho estava chegando à sua conclusão lógica. Respirei fundo, bebi café e apaguei a porra do caralho. É necessário ler menos a sua Internet! Chegou a hora pegue uma espingarda e lavagem cerebral:
- Um objeto deve ser formado com tipos de valor explícitos;
- Você pode usar genéricos para lançar tipos em uma única interface;
- Os genéricos suportam tipos padrão;
- O
type
construção é claramente capaz de outra coisa.
Escrevemos a interface genérica com tipos padrão:
interface IUser < TName = string, TAge = number, TAlt = string | number > { name: TName; age: TAge; phone: TAlt; }
Para Joi, você pode criar uma segunda interface, herdando a principal desta maneira:
interface IUserJoi extends IUser < Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema > {}
Não é bom o suficiente, porque o próximo desenvolvedor pode expandir o IUserJoi
com um coração leve ou pior. Uma opção mais limitada é obter um comportamento semelhante:
type IUserJoi = IUser<Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema>;
Tentamos:
const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' }; const joiUser: IUserJoi = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
UPD:
Para encerrar no Joi.object
tive que lutar com o erro TS2345
e a solução mais simples foi as any
. Eu acho que isso não é uma suposição crítica, porque o objeto acima ainda está na interface.
const joiUserInfo = { info: Joi.object(joiUser as any).required() };
Compila, parece elegante no local de uso e, na ausência de condições especiais, sempre define os tipos padrão! Beleza ...

... o que passei dois dias úteis
Sumário
Que conclusões podem ser tiradas de tudo isso:
- Obviamente, não aprendi a encontrar respostas para perguntas. Certamente, com uma solicitação bem-sucedida, essa solução (ou melhor ainda) está nos primeiros 5k links do mecanismo de pesquisa;
- Mudar para o pensamento estático da dinâmica não é tão fácil, com muito mais frequência eu apenas martelo esse enxame;
- Genéricos são legais. No Habr e o stackoverflow está cheio
de bicicletas soluções não óbvias para criar digitação forte ... fora do tempo de execução.
O que ganhamos:
- Ao alterar a interface, todo o código cai, incluindo o validador;
- No editor, apareceram dicas sobre nomes de propriedades e tipos de valores de objetos para escrever um validador;
- A falta de bibliotecas obscuras de terceiros para o mesmo objetivo;
- As regras Joi serão aplicadas somente quando necessário, em outros casos, tipos padrão;
- Se alguém quiser alterar o tipo de valor de uma propriedade, com a organização correta do código, ele irá para o local onde todos os tipos associados a essa propriedade são reunidos;
- Aprendemos a esconder belamente e simplesmente os genéricos por trás da abstração de
type
, descarregando visualmente o código de construções monstruosas.
Moral: a experiência não tem preço; para o resto, existe um mapa do mundo.
Você pode ver, tocar, executar o resultado final:
https://repl.it/@Melodyn/Joi-by-interface
