import { pipe, Result } from 'ts-data-forge'; import { type Brand, type Primitive, type StrictExtract } from 'ts-type-forge'; import { type Type } from '../type.mjs'; import { createType, type ValidationError } from '../utils/index.mjs'; export type { Brand } from 'ts-type-forge'; type ArrayToUnion = A extends readonly [] ? never : A[number]; export const brand = < const A extends StrictExtract, const BrandTrueKeys extends readonly string[], const BrandFalseKeys extends readonly string[] = readonly [], >({ baseType, brandFalseKeys, brandKeys, defaultValue: defaultValue_, is: is_, typeName, }: Readonly<{ baseType: Type; is?: ( a: A, ) => a is Brand, ArrayToUnion>; defaultValue?: A; typeName?: string; brandKeys: BrandTrueKeys; brandFalseKeys?: BrandFalseKeys; }>): Type< Brand, ArrayToUnion> > => { type T = Brand, ArrayToUnion>; const is: (a: A) => a is T = is_ ?? ((_a): _a is T => true); const defaultValue: A = defaultValue_ ?? baseType.defaultValue; if (!is(defaultValue)) { throw new Error( `defaultValue ${defaultValue} doesn't pass \`is\` function`, ); } const brandKeysStr = [ ...brandKeys.map((s) => `"${s}"`), ...(brandFalseKeys?.map((s) => `not("${s}")`) ?? []), ].join(' & '); const typeNameFilled = typeName ?? brandKeysStr; const validate: Type['validate'] = (a) => pipe(a) .map(baseType.validate) .map((res): Result => Result.isErr(res) ? res : is(res.value) ? Result.ok(res.value satisfies T) : Result.err([ { path: [], actualValue: res.value, expectedType: typeNameFilled, typeName: typeNameFilled, details: // If typeName is specified, it will be used in the error message, so no further information is required. typeName === undefined ? { kind: 'brand' as const, description: brandKeysStr, } : undefined, } satisfies ValidationError, ]), ).value; return createType({ typeName: typeNameFilled, defaultValue, validate, }); };