3

`is-kit` chegou a 50 stars no GitHub — veja como estamos usando em produção

Oi oi!

Eu sou @nyaomaru, engenheiro frontend tentando perder peso. 🐖🙀

Eu mantenho uma biblioteca de type guards chamada is-kit.

Passo uma quantidade nada razoável de tempo me perguntando:

“Mas e se esse valor for, na verdade, unknown???”

Recentemente, is-kit chegou a 50 stars no GitHub 🎉🎉🎉

Uma star não é um benchmark.

E 50 stars não transformam uma biblioteca automaticamente em algo pronto para produção.

Mas cada uma delas ainda significa:

“Alguém achou essa ideia útil.”

E isso me deixa muito feliz!!

Cada star me dá ainda mais motivação para continuar melhorando a biblioteca!!!!

Mas também tenho algo mais concreto para compartilhar:

is-kit agora é usado em uma aplicação TypeScript em produção que atende mais de 100.000 usuários. 🚀

Neste artigo, quero mostrar:

  • Qual era o problema
  • Como introduzimos is-kit
  • O que realmente mudou
  • Onde ele é usado hoje
  • Quais são suas vantagens práticas

Vamos lá!


📋 O problema não era “validação”

A aplicação já tinha várias pequenas verificações como estas 👇

typeof value === "string";
typeof value === "number";
value === null || value === undefined;

Também havia type guards definidos pela própria aplicação para:

  • Erros do cliente HTTP
  • Status codes
  • Literal unions
  • Arrays
  • Plain objects
  • Valores vindos de JSON ou respostas de API

Cada verificação fazia sentido isoladamente.

O problema aparecia quando elas começavam a se repetir.

Por exemplo, vários error guards tinham praticamente a mesma estrutura.

type HttpClientError<T = unknown> = Error & {
  isHttpClientError: true;
  response?: {
    status: number;
    data: T;
  };
};

function isUnauthorizedError(error: unknown): error is HttpClientError {
  return (
    !!error &&
    (error as HttpClientError).isHttpClientError === true &&
    (error as HttpClientError).response?.status === 401
  );
}

function isValidationError(error: unknown): error is HttpClientError {
  return (
    !!error &&
    (error as HttpClientError).isHttpClientError === true &&
    (error as HttpClientError).response?.status === 422
  );
}

Isso funciona.

Mas existem três problemas práticos:

  1. A mesma verificação base é repetida
  2. Assertion casts aparecem dentro de cada guard
  3. Adicionar outro status significa criar outra cópia

O código não estava quebrado.

Ele simplesmente estava pedindo uma abstração reutilizável. 🔧


🏃‍♂️ O padrão usado em produção

Substituímos essas verificações repetidas por pequenos guards que podem ser combinados.

Aqui está uma versão sem detalhes específicos do negócio, baseada no padrão usado em produção.

import { define, equalsKey, or } from "is-kit";

type HttpClientError<T = unknown> = Error & {
  isHttpClientError: true;
  code?: string;
  response?: {
    status: number;
    data: T;
  };
};

const isHttpClientError = define<HttpClientError>((value) =>
  equalsKey("isHttpClientError", true)(value),
);

const isHttpErrorWithStatus = (status: number) =>
  define<HttpClientError>(
    (value) => isHttpClientError(value) && value.response?.status === status,
  );

export const isUnauthorizedError = isHttpErrorWithStatus(401);

export const isValidationError = isHttpErrorWithStatus(422);

const hasTimeoutCode = define<HttpClientError>(
  (value) => isHttpClientError(value) && value.code === "TIMEOUT",
);

const hasTimeoutMessage = define<HttpClientError>(
  (value) => isHttpClientError(value) && value.message.includes("timed out"),
);

export const isTimeoutError = or(hasTimeoutCode, hasTimeoutMessage);

Há alguns detalhes importantes aqui.

define

define<T> transforma uma verificação booleana em runtime em um predicate reutilizável.

const isHttpErrorWithStatus = (status: number) =>
  define<HttpClientError>(...);

A responsabilidade ainda é nossa.

A verificação em runtime precisa realmente provar que o valor é T.

is-kit não consegue transformar um predicate incorreto em um predicate correto.

Mas ele permite que custom guards tenham uma estrutura consistente.

equalsKey

O erro base não é simplesmente um JSON ou plain object.

É uma instância de erro com uma propriedade usada como marcador.

Por isso, um schema para plain objects não é a abstração adequada aqui.

equalsKey("isHttpClientError", true)

expressa exatamente o que precisamos:

“Este valor possui esta chave, e o valor dela é exatamente true.”

or

Um timeout pode ser identificado de mais de uma maneira.

Em vez de criar outra grande condição, podemos combinar dois guards reutilizáveis.

const isTimeoutError = or(hasTimeoutCode, hasTimeoutMessage);

Essa é a ideia central do is-kit:

Crie guards pequenos e depois combine-os.


✨ O que realmente mudou

A primeira refatoração usando is-kit não foi simplesmente isto 👇

pnpm add is-kit

Ela mudou a estrutura da camada de guards.

Resultado observávelMudança
Error guards7 módulos separados viraram 1 módulo compartilhado
Diff da adoção335 linhas adicionadas, 584 removidas
Diff líquido249 linhas a menos
Imports diretos hojeis-kit está isolado em 7 módulos auxiliares da aplicação
Alcance na aplicaçãoEsses helpers são usados por 39 arquivos de source que não são testes

O diff inclui testes reescritos e helper adapters.

Portanto, 249 linhas a menos não significa que uma biblioteca magicamente apagou código.

Esse é simplesmente o resultado medido daquela consolidação específica.

A mudança mais importante é a estrutura.

primitivas do is-kit
        ↓
helpers de guards da aplicação
        ↓
features, routes, services e UI

A aplicação em produção não importa is-kit diretamente em todos os componentes.

Na maioria dos lugares, o código usa helpers pertencentes à própria aplicação.


🤔 Por que manter uma camada própria da aplicação?

Para guards primitivos, a aplicação encapsula ou reexporta os guards fornecidos pela biblioteca 👇

import {
  isNumber as isFiniteNumberGuard,
  isNumberPrimitive,
  isString as isStringGuard,
} from "is-kit";

export const isString = isStringGuard;
export const isNumber = isNumberPrimitive;
export const isFiniteNumber = isFiniteNumberGuard;

Isso pode parecer um detalhe pequeno, mas é uma decisão de design importante.

JavaScript possui mais de um significado útil para “number”.

typeof NaN === "number";
typeof Infinity === "number";

Na aplicação:

  • isNumber segue a semântica primitiva de typeof
  • isFiniteNumber rejeita NaN e Infinity

A aplicação é dona desses nomes.

is-kit fornece a implementação reutilizável.

Essa camada também significa que:

  • Os call sites não dependem das decisões de naming da biblioteca
  • A semântica permanece consistente em toda a aplicação
  • Uma futura migração possui um único ponto claro por onde começar

É assim que prefiro introduzir pequenas bibliotecas em aplicações grandes.

Adote-as por trás de um vocabulário local.


😎 Outros padrões reais de uso

Os HTTP error guards são o maior exemplo, mas não são o único.

Arrays

import { arrayOf, isNumberPrimitive } from "is-kit";

export const isNumberArray = arrayOf(isNumberPrimitive);

Isso substitui:

const isNumberArray = (value: unknown): value is number[] =>
  Array.isArray(value) &&
  value.every((item): item is number => typeof item === "number");

Literal unions

import { oneOfValues } from "is-kit";

const VIEW_MODES = ["compact", "comfortable"] as const;

const isViewMode = oneOfValues(VIEW_MODES);

declare const input: unknown;

if (isViewMode(input)) {
  // "compact" | "comfortable"
  input;
}

Valores nullish

import { isNull, isUndefined, or } from "is-kit";

export const isNullish = or(isNull, isUndefined);

Como isso é uma função, podemos reutilizá-la diretamente.

const definedItems = items.filter((item) => !isNullish(item));

A aplicação atual usa a mesma ideia para:

  • Branching de erros
  • Valores derivados de JSON e APIs
  • Filtragem de collections que podem conter valores nullable
  • Verificações de literal values
  • Valores de UI que podem ser strings ou outros valores renderizáveis

É assim que o uso em produção se parece na prática.

Não é um schema gigantesco,

mas várias pequenas decisões em pontos normais do control flow.


🚀 As vantagens práticas

Depois de usar is-kit na aplicação, algumas vantagens ficaram mais claras.

1. Adoção incremental

Não precisamos redesenhar a data layer.

Uma verificação como:

typeof value === "string";

pode virar:

isString(value);

E depois, quando a reutilização fizer sentido:

values.filter(isString);

2. Menos assertion casts

Os antigos error guards usavam repetidamente:

error as HttpClientError;

Na versão composta, fazemos o narrowing uma vez e depois acessamos o valor já reduzido normalmente:

isHttpClientError(value) && value.response?.status === status;

3. Semântica de runtime compartilhada

Perguntas como estas agora possuem respostas explícitas:

  • “number” inclui NaN?
  • Este object check aceita instâncias de classes?
  • Este campo é optional, nullable ou os dois?
  • Dois valores são comparados usando semântica de === ou Object.is?

O benefício não é apenas uma sintaxe menor.

É ter menos respostas ligeiramente diferentes espalhadas pelo codebase.

4. Control flow normal do TypeScript

O resultado continua sendo apenas uma função.

if (isValidationError(error)) {
  error.response?.data;
}

Nenhum parse result é necessário.

Nenhum schema object precisa circular pela aplicação.

Isso torna os guards fáceis de usar em:

  • if
  • filter
  • event handlers
  • error boundaries
  • utility functions

5. Pequena superfície de dependências

is-kit não possui runtime dependencies.

Isso não significa que ele tenha custo zero no bundle.

Significa que adicioná-lo não traz junto uma árvore de dependências transitivas em runtime.


👮 Virou uma regra do time

Um sinal de adoção real é quando uma biblioteca deixa de ser apenas uma preferência individual.

O repositório de produção agora possui uma regra para contribuidores:

Ao combinar guards de is-kit, prefira or, and, andAll, nullish e outros combinators relacionados, em vez de reconstruir a mesma composição usando operadores nativos.

Por exemplo:

const isTextOrNumber = or(isString, isNumberPrimitive);

em vez de:

const isTextOrNumber = (value: unknown) =>
  isString(value) || isNumberPrimitive(value);

As duas versões podem retornar o mesmo boolean.

Mas a primeira é um guard nomeado e reutilizável, que pode ser passado para outras funções e combinado novamente.

Essa regra também é usada pelos coding agents que trabalham no repositório.

Isso importa porque uma ferramenta não foi realmente adotada se cada contributor, humano ou IA, inventa um estilo diferente.


✖️ O que não podemos afirmar

Quero ter cuidado aqui.

Não fizemos um estudo controlado demonstrando que is-kit:

  • Melhorou a performance em runtime
  • Reduziu incidentes em produção
  • Tornou toda tarefa de validação mais fácil

Então não vou afirmar nenhuma dessas coisas.

Os efeitos que realmente conseguimos observar são:

  • Guards repetidos foram consolidados
  • Verificações com muitos assertions viraram predicates compostos
  • A semântica das primitivas foi centralizada
  • O código da aplicação ganhou narrowing functions reutilizáveis
  • O padrão passou a fazer parte das guidelines do repositório

Trata-se principalmente de uma melhoria de manutenibilidade e type safety. 🏋️‍♂️


👀 Por que não usar uma schema library?

Para esses call sites, não precisávamos de:

  • Árvores detalhadas de erros de validação
  • Transformações de dados
  • Um modelo schema-first

Precisávamos responder:

“Este valor unknown pode entrar com segurança neste branch?”

É exatamente nesse ponto que um type guard faz sentido.

Para forms, contratos de API ou erros de validação detalhados, uma schema library como Zod ainda pode ser a ferramenta mais adequada.

São problemas diferentes.


🎯 O que 50 stars significam para mim

50 stars é pouco quando comparado às maiores bibliotecas TypeScript.

Mas open source não se torna significativo somente depois de milhares de stars.

Para mim, esse marco significa que:

  • Pessoas de fora do projeto entendem a ideia
  • A API é útil além de um simples exemplo de brinquedo
  • A biblioteca está resolvendo um problema real de manutenção
  • Ainda há muita coisa para melhorar

E o uso em uma aplicação em produção dá um pouco mais de peso a esse marco.

is-kit não está apenas recebendo stars.

Hoje ele ajuda código real de aplicações a responder:

“Que valor é este, e o TypeScript pode confiar nele?”

Obrigado a todos que deram uma star, testaram, reportaram uma issue ou simplesmente deram uma olhada no repositório.

Se pequenos type guards compostos combinam com a forma como você escreve TypeScript, experimente 👇

https://github.com/nyaomaru/is-kit

Carregando publicação patrocinada...