Generic Object Property Formatter and Validator using Typescript
09 May 2023
Updated: 03 September 2023
At times there’s a need for transforming specific object properties from an input format to some specific output, this can be done using the following code in some generic means
The Concept of a Schema
The concept of a schema to be used for formatting some data can be defined as follows
1export type Primitive = string | number | boolean | Symbol | Date2
3/** * Define a formatter that can take in a generic object type and format each entry of that object */4export type FormatterSchema<T> = {5 [K in keyof T]?: T[K] extends Primitive6 ? // if the value is primitive then we can just transform it using a simple function7 (val: T[K]) => T[K]8 : // if it's an object then it should either be a formatter schema or a transformer function9 FormatterSchema<T[K]> | ((val: T[K]) => T[K])10}
An Interpreter
The implementation of an interpreter that satisfies the above can done as follows:
1type Formatter<T> = (val: T) => T2const isFormatterFn = <T>(3 formatter: FormatterSchema<T>[keyof T]4): formatter is Formatter<T[keyof T]> => typeof formatter === 'function'5
6const isFormatterObj = <T>(7 formatter: FormatterSchema<T>[keyof T]8): formatter is Exclude<Formatter<T[keyof T]>, Function> =>9 typeof formatter === 'object'10
11export const interpret =12 <T>(schema: FormatterSchema<T>): Formatter<T> =>13 (val) => {14 const keys = Object.keys(schema) as Array<keyof T>15 return keys.reduce<T>((prev, key) => {16 const keySchema = schema[key]17 const keyVal = val[key]18
19 const isSchemaFn = isFormatterFn(keySchema)20 if (isSchemaFn) {21 return {22 ...prev,23 [key]: keySchema(keyVal),24 }25 }26
27 const isSchemaObj = isFormatterObj(keySchema)28 if (isSchemaObj) {29 return {30 ...prev,31 [key]: interpret(keySchema)(keyVal),32 }33 }34
35 return prev36 }, val)37 }
Transformers
We can define some general transformers, for example:
1const trunc = Math.trunc2
3const round = Math.round4
5const length = (len: number) => (val: string) => val.slice(0, len)6
7const trim = (val: string) => val.trim()8
9const constant =10 <T>(val: T) =>11 () =>12 val13
14const optional =15 <T>(fn: (val: T) => T) =>16 (val?: T) =>17 val === undefined ? undefined : fn(val)18
19export const formatter = {20 trunc,21 round,22 length,23 trim,24 constant,25 optional,26}
Usage
For the sake of example we can define a data type to use
1type User = {2 name: string3 location: {4 address: string5 city: string6 gps?: [number, number]7 }8}9
10const user: User = {11 name: 'Bob Smithysmithson',12 location: {13 city: 'Somewhere secret',14 address: '123 Secret street',15 gps: [123, 456],16 },17}
Using as a Transformer
We can use the above by defining the transformer and using it to transform the input data according to the defined schema
1const format = interpret<User>({2 name: formatter.length(10),3 location: {4 city: (val) => (['Home', 'Away'].includes(val) ? val : 'Other'),5 gps: (val) => (isValidGPS(val) ? val : undefined),6 },7})8
9const result = format(user)10
11console.log(result)12// {13// name: 'Bob Smithy',14// location: {15// city: 'Other',16// adddtess: '123 Secret street',17// gps: undefined,18// }19// }
Using as a Validator
Using the same principal as above we can use it for validaing data. Validation can be done by throwing in the transformer function
1const validate = interpret<User>({2 name: (val) => {3 if (val.length > 5) {4 throw 'Length too long'5 }6
7 return val8 },9})10
11// throws with Length too long12validate({13 name: 'Jack Smith',14})
Exploration
It may be worth taking the concept and expanding it into a more fully fledged library with a way to handle validations more simply or to provide more builtin transformers and validators but for the moment it can be stated that there are enough javascript libraries for validation and the formatter alone is likely of limited value but may be worth exploring further if gaps in the existing technology are found
Regardless of the utility of the particular type above I think the concept and types give some insight into how one can build out a library as the ones discussed above
Working Example
A working playground with the code from here can be found at The Typescript Playground