smart-types-ts
Version:
A collection of _Smart Types_ and _Smart Constructors_ which enable you to be more strict when defining your application's important types/interfaces.
98 lines (89 loc) • 2.83 kB
text/typescript
import { sequenceS } from "fp-ts/lib/Apply";
import * as e from "fp-ts/lib/Either";
import { pipe } from "fp-ts/lib/function";
import { getObjectSemigroup } from "fp-ts/lib/Semigroup";
import {
EnforceNonEmptyRecord,
SmartConstructor,
SmartConstructorOptional
} from "./utilTypes";
/**
*
* Given an object replaces all the values recursively with strings to describe an arbitrarily
* nested object of validation errors
*
* @example
* ```ts
* type Person = {
* name: {
* display: StringOfLength<1, 30>
* full: StringOfLength<1, 100>
* },
* profilePicture: URL
* }
*
* FieldError<Person> --> {
* name?: {
* display?: string;
* full?: string;
* };
* profilePicture?: string;
* }
* ```
*/
type FieldError<
T extends EnforceNonEmptyRecord<Record<PropertyKey, unknown>>
> = Partial<
{
[key in keyof T]: T[key] extends Record<PropertyKey, unknown>
? FieldError<T[key]>
: string;
}
>;
/**
* A ConstructorObj is the object passed to the mkSmartObject function. It should have
* keys of the object to construct and values which are Smart Constructors. This types works
* recursively to allow for nested Smart Objects.
*/
type ConstructorObj<T extends Record<PropertyKey, unknown>> = {
[key in keyof T]:
| SmartConstructor<T[key]>
| SmartConstructorOptional<T[key]>
// Allow for nested SmartObjects
| (T[key] extends Record<PropertyKey, unknown>
? MkSmartObject<T[key]>
: never);
};
type MkSmartObject<T extends Record<PropertyKey, unknown>> = (
input: Record<PropertyKey, unknown>
) => e.Either<FieldError<T>, T>;
// Stricter typing for Object.keys
const keys: <T extends Record<PropertyKey, unknown>>(obj: T) => Array<keyof T> =
Object.keys;
export const mkSmartObject = <
T extends EnforceNonEmptyRecord<Record<PropertyKey, unknown>>
>(
construct: ConstructorObj<T>
): MkSmartObject<T> => simpleObj => {
const validationResultsObj = keys(construct).reduce(
(accum, fieldName) => {
const inputVal = simpleObj[fieldName];
const smartConstructor: any = construct[fieldName];
accum[fieldName] = pipe(
smartConstructor(inputVal),
e.mapLeft(error => ({ [fieldName]: error }))
);
return accum;
},
{} as any
);
return pipe(
sequenceS(e.getValidation(getObjectSemigroup<FieldError<T>>()))(
validationResultsObj
),
// TODO try harder to actually make this crazy function compile although I'm not sure
// it actually matters. If it's not possible, this module should have very good test
// coverage to compensate for the lack of help from the compiler here.
e.map(x => x as any)
);
};