UNPKG

react-router-typesafe-routes

Version:

Enhanced type safety via validation for all route params in React Router v7.

216 lines (215 loc) 10.3 kB
import { parser } from "./parser.mjs"; function configure({ parserFactory }) { const type = createType({ parserFactory: parserFactory }); function string(arg1, arg2) { const resolvedValidator = typeof arg1 === "function" ? arg1 : identity; const resolvedParser = typeof arg1 === "function" ? arg2 : arg1; return type((value) => (value === undefined ? value : resolvedValidator(stringValidator(value))), resolvedParser !== null && resolvedParser !== void 0 ? resolvedParser : parserFactory("string")); } function number(arg1, arg2) { const resolvedValidator = typeof arg1 === "function" ? arg1 : identity; const resolvedParser = typeof arg1 === "function" ? arg2 : arg1; return type((value) => (value === undefined ? value : resolvedValidator(numberValidator(value))), resolvedParser !== null && resolvedParser !== void 0 ? resolvedParser : parserFactory("number")); } function boolean(arg1, arg2) { const resolvedValidator = typeof arg1 === "function" ? arg1 : identity; const resolvedParser = typeof arg1 === "function" ? arg2 : arg1; return type((value) => (value === undefined ? value : resolvedValidator(booleanValidator(value))), resolvedParser !== null && resolvedParser !== void 0 ? resolvedParser : parserFactory("boolean")); } function date(arg1, arg2) { const resolvedValidator = typeof arg1 === "function" ? arg1 : identity; const resolvedParser = typeof arg1 === "function" ? arg2 : arg1; return type((value) => (value === undefined ? value : resolvedValidator(dateValidator(value))), resolvedParser !== null && resolvedParser !== void 0 ? resolvedParser : parserFactory("date")); } function union(value, parser) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const values = Array.isArray(value) ? value : getEnumValues(value); const defaultParser = parser !== null && parser !== void 0 ? parser : parserFactory("unknown"); return type((value) => { if (value !== undefined && (!(typeof value === "string" || typeof value === "number" || typeof value === "boolean") || !values.includes(value))) { throw new Error(`${String(value)} is not assignable to '${values.map((item) => JSON.stringify(item)).join(" | ")}'`); } return value; }, { stringify(value, context) { return defaultParser.stringify(value, Object.assign(Object.assign({}, context), { hint: typeof value })); }, parse(value, context) { for (const canonicalValue of values) { try { if (canonicalValue === defaultParser.parse(value, Object.assign(Object.assign({}, context), { hint: typeof canonicalValue }))) { return canonicalValue; } } catch (_a) { // Try next value } } throw new Error(`${String(value)} is not assignable to '${values.map((item) => JSON.stringify(item)).join(" | ")}'`); }, }); } return { type, string, number, boolean, date, union, }; } function createType({ parserFactory }) { return function type(validator, parser = parserFactory("unknown")) { const serializeParam = (value) => parser.stringify(value, { kind: "pathname" }); const deserializeParam = (value) => validator(typeof value === "undefined" ? value : parser.parse(value, { kind: "pathname" })); const serializeSearchParam = (value) => parser.stringify(value, { kind: "search" }); const deserializeSearchParam = (value) => validator(typeof value[0] === "undefined" ? value[0] : parser.parse(value[0], { kind: "search" })); const serializeHash = (value) => parser.stringify(value, { kind: "hash" }); const deserializeHash = (value) => validator(typeof value === "undefined" ? value : parser.parse(value, { kind: "hash" })); const serializeState = (value) => value; const deserializeState = (value) => validator(value); return Object.assign({}, // TODO: Remove later. ATM typescript picks the wrong function overload without this. { serializeParam: serializeParam, deserializeParam: ensureNoError(deserializeParam), serializeSearchParam: serializeSearchParam, deserializeSearchParam: ensureNoError(deserializeSearchParam), serializeHash: serializeHash, deserializeHash: ensureNoError(deserializeHash), serializeState: serializeState, deserializeState: ensureNoError(deserializeState), }, { array: getArrayParamTypeBuilder(ensureNoError(validator), { stringify: parser.stringify, parse: ensureNoError(parser.parse), }), }, { default: (def) => { const validDef = validateDef(validator, def); return Object.assign({}, // TODO: Remove later. ATM typescript picks the wrong function overload without this. { serializeParam: serializeParam, deserializeParam: ensureNoUndefined(ensureNoError(deserializeParam), validDef), serializeSearchParam: serializeSearchParam, deserializeSearchParam: ensureNoUndefined(ensureNoError(deserializeSearchParam), validDef), serializeHash: serializeHash, deserializeHash: ensureNoUndefined(ensureNoError(deserializeHash), validDef), serializeState: serializeState, deserializeState: ensureNoUndefined(ensureNoError(deserializeState), validDef), }, { array: getArrayParamTypeBuilder(ensureNoUndefined(ensureNoError(validator), validDef), { stringify: parser.stringify, parse: ensureNoUndefined(ensureNoError(parser.parse), validDef), }), }); }, defined: () => { return Object.assign({}, // TODO: Remove later. ATM typescript picks the wrong function overload without this. { serializeParam: serializeParam, deserializeParam: ensureNoUndefined(deserializeParam), serializeSearchParam: serializeSearchParam, deserializeSearchParam: ensureNoUndefined(deserializeSearchParam), serializeHash: serializeHash, deserializeHash: ensureNoUndefined(deserializeHash), serializeState: serializeState, deserializeState: ensureNoUndefined(deserializeState), }, { array: getArrayParamTypeBuilder(ensureNoUndefined(validator), { stringify: parser.stringify, parse: ensureNoUndefined(parser.parse), }), }); }, }); }; } // TODO: Find a way to preserve <T,> without prettier-ignore // prettier-ignore const getArrayParamTypeBuilder = (validator, parser) => () => { const serializeSearchParam = (values) => values.filter(isDefined).map((value) => parser.stringify(value, { kind: 'search' })); const deserializeSearchParam = (values) => values.map((item) => validator(parser.parse(item, { kind: 'search' }))).filter(isDefined); const serializeState = (values) => values; const deserializeState = (values) => (Array.isArray(values) ? values : []).map((item) => validator(item)).filter(isDefined); return { serializeSearchParam: serializeSearchParam, deserializeSearchParam: deserializeSearchParam, serializeState: serializeState, deserializeState: deserializeState, }; }; function isDefined(value) { return typeof value !== "undefined"; } function ensureNoError(fn) { return (...args) => { try { return fn(...args); } catch (error) { return undefined; } }; } function ensureNoUndefined(fn, def) { return (...args) => { const result = fn(...args); if (result === undefined) { if (def === undefined) { throw new Error("Can't return 'undefined' for a .defined() param. Use .default() (or omit the modifier) instead. Remember, required pathname params use .defined() by default."); } return def; } return result; }; } function validateDef(validator, def) { const validDef = validator(def); if (validDef === undefined) { throw new Error("Default value validation resulted in 'undefined', which is forbidden"); } return validDef; } function stringValidator(value) { if (typeof value !== "string") { throw new Error(`${String(value)} is not assignable to 'string'`); } return value; } function numberValidator(value) { if (typeof value !== "number") { throw new Error(`${String(value)} is not assignable to 'number'`); } if (Number.isNaN(value)) { throw new Error(`Unexpected NaN`); } return value; } function booleanValidator(value) { if (typeof value !== "boolean") { throw new Error(`${String(value)} is not assignable to 'boolean'`); } return value; } function dateValidator(value) { if (!(value instanceof Date)) { throw new Error(`${String(value)} is not assignable to 'Date'`); } if (typeof value !== "undefined" && Number.isNaN(value.getTime())) { throw new Error("Unexpected Invalid Date"); } return value; } function getEnumValues(enumObj) { return Object.keys(enumObj) .filter((key) => typeof enumObj[enumObj[key]] !== "number") .map((key) => enumObj[key]); } function identity(value) { return value; } const { type, string, number, boolean, date, union } = configure({ parserFactory: parser }); export { configure, type, string, number, boolean, date, union, };