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
JavaScript
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, };