Pular para o conteúdo principal

Criando TypeFields

Este guia documenta o processo completo para criar um novo TypeField no TyForge. Todo TypeField segue uma estrutura obrigatória com métodos padronizados.

Quando criar um TypeField​

Se o projeto consumidor precisar de um formato que não existe (ex: CPF, CNPJ, telefone, moeda), o TypeField deve ser criado dentro do TyForge em src/type-fields/. A filosofia do projeto é zero boilerplate: instalar, importar, usar.

Estrutura obrigatória​

Arquivo: src/type-fields/{nome-em-ingles}.typefield.ts

O exemplo abaixo mostra a estrutura completa de um TypeField para CPF:

import { TypeField, TValidationLevel } from "@tyforge/type-fields/type-field.base";
import { ITypeFieldConfig } from "@tyforge/type-fields/type-field.config";
import { Result, ok, err, isFailure, OK_TRUE } from "@tyforge/result";
import { ExceptionValidation } from "@tyforge/exceptions/validation.exception";
import { TypeGuard } from "@tyforge/tools/type_guard";

export type TCpf = string;
export type TCpfFormatted = string;

export class FCpf extends TypeField<TCpf, TCpfFormatted> {
override readonly typeInference = "FCpf";

override readonly config: ITypeFieldConfig<TCpf> = {
jsonSchemaType: "string",
serializeAsString: false,
minLength: 11,
maxLength: 14,
};

private constructor(value: TCpf, fieldPath: string) {
super(value, fieldPath);
}

protected override validateRules(
value: TCpf,
fieldPath: string,
validateLevel: TValidationLevel = "full",
): Result<true, ExceptionValidation> {
const base = super.validateRules(value, fieldPath, validateLevel);
if (!base.success) return base;
if (validateLevel !== "full") return OK_TRUE;

// Regras de negócio específicas do CPF
const digits = value.replace(/\D/g, "");
if (digits.length !== 11) {
return err(ExceptionValidation.create(fieldPath, "CPF must have exactly 11 digits"));
}

return OK_TRUE;
}

static validateType(value: unknown, fieldPath: string): Result<TCpf, ExceptionValidation> {
return TypeGuard.isString(value, fieldPath);
}

static create<T = TCpf>(raw: T, fieldPath = "Cpf"): Result<FCpf, ExceptionValidation> {
const typed = FCpf.validateType(raw, fieldPath);
if (isFailure(typed)) return err(typed.error);
const instance = new FCpf(typed.value, fieldPath);
const rules = instance.validateRules(typed.value, fieldPath, TypeField.createLevel);
if (!rules.success) return err(rules.error);
return ok(instance);
}

static createOrThrow(value: TCpf, fieldPath = "Cpf"): FCpf {
const result = FCpf.create(value, fieldPath);
if (isFailure(result)) throw result.error;
return result.value;
}

static assign<T = TCpf>(value: T, fieldPath = "Cpf"): Result<FCpf, ExceptionValidation> {
const typed = FCpf.validateType(value, fieldPath);
if (isFailure(typed)) return err(typed.error);
const instance = new FCpf(typed.value, fieldPath);
const rules = instance.validateRules(typed.value, fieldPath, TypeField.assignLevel);
if (!rules.success) return err(rules.error);
return ok(instance);
}

override formatted(): TCpfFormatted {
return this.getValue();
}

override toString(): string {
return this.getValue();
}

override getDescription(): string {
return "Brazilian individual taxpayer identification number (CPF).";
}

override getShortDescription(): string {
return "CPF";
}
}

Anatomia de cada método​

MétodoTipoResponsabilidade
constructorprivateRecebe value e fieldPath, repassa ao super
validateTypestaticNarrowing de tipo — usa TypeGuard, retorna Result<TPrimitive>
create<T>staticValidação completa — validateType + validateRules com TypeField.createLevel
createOrThrowstaticWrapper que faz throw em caso de falha
assign<T>staticHidratação — validateType + validateRules com TypeField.assignLevel
validateRulesprotected overrideRegras de negócio — chama super.validateRules() primeiro, depois regras extras
formatted()overrideRepresentação formatada do valor
toString()overrideRepresentação textual do valor
getDescription()overrideDescrição longa em inglês
getShortDescription()overrideDescrição curta em inglês

Propriedades obrigatórias​

PropriedadeTipoDescrição
typeInferenceoverride readonlyNome da classe como string literal para inferência de tipo
configoverride readonly ITypeFieldConfig<T>Configuração com jsonSchemaType e serializeAsString obrigatórios

Registrar exports​

Após criar o arquivo do TypeField, registrar os exports em dois locais.

1. src/type-fields/index.ts​

export { FCpf, TCpf, TCpfFormatted } from "./cpf.typefield";

Exportar sempre os três artefatos: a classe F, o tipo primitivo T e o tipo formatado TFormatted.

2. src/index.ts​

Na seção de Type Fields:

export { FCpf } from "./type-fields/cpf.typefield";

Na API pública, exportar apenas a classe.

Enums como TypeField​

TypeFields que representam um conjunto fechado de valores utilizam um objeto as const com prefixo O:

import { TypeField } from "@tyforge/type-fields/type-field.base";
import { ITypeFieldConfig } from "@tyforge/type-fields/type-field.config";
import { Result, ok, err, isFailure } from "@tyforge/result";
import { ExceptionValidation } from "@tyforge/exceptions/validation.exception";
import { TypeGuard } from "@tyforge/tools/type_guard";

export const OPaymentMethod = {
CREDIT: "credit",
DEBIT: "debit",
PIX: "pix",
} as const;

export type TKeyPaymentMethod = keyof typeof OPaymentMethod;
export type TPaymentMethod = (typeof OPaymentMethod)[TKeyPaymentMethod];
export type TPaymentMethodFormatted = string;

export class FPaymentMethod extends TypeField<TPaymentMethod, TPaymentMethodFormatted> {
override readonly typeInference = "FPaymentMethod";

override readonly config: ITypeFieldConfig<TPaymentMethod> = {
jsonSchemaType: "string",
serializeAsString: false,
minLength: 1,
maxLength: 20,
validateEnum: OPaymentMethod,
};

private constructor(value: TPaymentMethod, fieldPath: string) {
super(value, fieldPath);
}

static validateType(
value: unknown,
fieldPath: string,
): Result<TPaymentMethod, ExceptionValidation> {
const str = TypeGuard.isString(value, fieldPath);
if (isFailure(str)) return err(str.error);
return TypeField.resolveEnum(OPaymentMethod, str.value, fieldPath);
}

static create<T = TPaymentMethod>(
raw: T,
fieldPath = "PaymentMethod",
): Result<FPaymentMethod, ExceptionValidation> {
const typed = FPaymentMethod.validateType(raw, fieldPath);
if (isFailure(typed)) return err(typed.error);
const instance = new FPaymentMethod(typed.value, fieldPath);
const rules = instance.validateRules(typed.value, fieldPath, TypeField.createLevel);
if (!rules.success) return err(rules.error);
return ok(instance);
}

static createOrThrow(raw: TPaymentMethod, fieldPath = "PaymentMethod"): FPaymentMethod {
const result = this.create(raw, fieldPath);
if (isFailure(result)) throw result.error;
return result.value;
}

static assign<T = TPaymentMethod>(
value: T,
fieldPath = "PaymentMethod",
): Result<FPaymentMethod, ExceptionValidation> {
const typed = FPaymentMethod.validateType(value, fieldPath);
if (isFailure(typed)) return err(typed.error);
const instance = new FPaymentMethod(typed.value, fieldPath);
const rules = instance.validateRules(typed.value, fieldPath, TypeField.assignLevel);
if (!rules.success) return err(rules.error);
return ok(instance);
}

override toString(): string {
return String(this.getValue());
}

override formatted(): TPaymentMethodFormatted {
return String(this.getValue());
}

override getDescription(): string {
return "Payment method type.";
}

override getShortDescription(): string {
return "Payment method";
}
}

Diferenças em relação ao TypeField padrão​

  1. Objeto const O: declarado antes da classe com prefixo O e as const
  2. Type alias TKey: keyof typeof O{Nome} para as chaves do enum
  3. Tipo do valor: derivado do objeto — (typeof O{Nome})[TKey{Nome}]
  4. validateEnum no config: o campo validateEnum na configuração habilita validação automática contra o enum pela classe base
  5. validateType com resolveEnum: usa TypeField.resolveEnum() para validar que o valor pertence ao enum

Registrar exports de enums​

No barrel, exportar também o objeto O:

// src/type-fields/index.ts
export {
FPaymentMethod, TPaymentMethod, TPaymentMethodFormatted, OPaymentMethod,
} from "./payment-method.typefield";

TypeFields locale-aware​

TypeFields que dependem de localidade (ex: FBankCode, FBankBranch, FStateCode, FBankAccountNumber) utilizam TypeField.localeRegion para aplicar regras específicas no validateRules(). O método formatted("data") utiliza TypeField.localeData para formatar valores destinados a APIs ou persistência. Não há herança por locale — uma única classe com lógica condicional via switch exaustivo:

protected override validateRules(
value: TBankCode,
fieldPath: string,
validateLevel: TValidationLevel = "full",
): Result<true, ExceptionValidation> {
const base = super.validateRules(value, fieldPath, validateLevel);
if (!base.success) return base;
if (validateLevel !== "full") return OK_TRUE;

// Validações genéricas (todos os locales)
if (!DIGITS_REGEX.test(value)) {
return err(ExceptionValidation.create(fieldPath, "Bank code must contain only numeric digits"));
}

// Validações específicas por locale
switch (TypeField.localeRegion) {
case "us":
break;
case "br":
if (!ISPB_REGEX.test(value)) {
return err(
ExceptionValidation.create(fieldPath, "ISPB bank code must be exactly 8 numeric digits"),
);
}
break;
default:
TypeField.assertNeverLocale(TypeField.localeRegion);
}

return OK_TRUE;
}

Regras do padrão locale-aware​

  • O switch deve ser exaustivo: todos os locales suportados devem ter um case, e o default deve chamar TypeField.assertNeverLocale() para garantir verificação em tempo de compilação
  • Regras de localidade são aplicadas apenas quando validateLevel === "full"
  • O locale padrão é "us" (Estados Unidos)
  • Para configurar o locale, usar TypeField.configure({ localeRegion: "br", localeData: "br" }) no bootstrap da aplicação
  • Locales suportados: "us" (Estados Unidos), "br" (Brasil)
  • localeData controla formatação para API/persistência via formatted("data")

Checklist de validação​

Antes de considerar um TypeField como concluído, verificar todos os itens:

  • Arquivo em src/type-fields/{nome}.typefield.ts
  • Exporta 2 type aliases: T{Nome} + T{Nome}Formatted
  • Nome da classe em inglês com prefixo F
  • Constructor é private
  • override readonly typeInference declarado
  • override readonly config: ITypeFieldConfig<T> com jsonSchemaType e serializeAsString
  • validateType(value, fieldPath) -- static, retorna Result<TPrimitive, ExceptionValidation>
  • create<T = TPrimitive>(raw: T, fieldPath?) -- genérico para aceitar unknown
  • createOrThrow(value, fieldPath?)
  • assign<T = TPrimitive>(value: T, fieldPath?) -- genérico para aceitar unknown
  • validateRules(value, fieldPath, validateLevel) -- protected override, chama super.validateRules() + regras extras
  • formatted(), toString(), getDescription(), getShortDescription() -- override
  • TypeGuard para narrowing (sem typeof manual, sem cast)
  • TypeField.createLevel / TypeField.assignLevel em vez de hardcode
  • Zero as casts
  • Zero any
  • Zero ! non-null assertions
  • Zero tyforge-lint-disable
  • Exportado em src/type-fields/index.ts (3 exports: F + T + TFormatted)
  • Exportado em src/index.ts
  • npm run typecheck passa
  • npm run test passa
  • npx tyforge-lint -- zero erros e zero warnings