Generic Object Property Formatter and Validator using Typescript
Created: 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
export type Primitive = string | number | boolean | Symbol | Date
/** * Define a formatter that can take in a generic object type and format each entry of that object */
export type FormatterSchema<T> = {
[K in keyof T]?: T[K] extends Primitive
? // if the value is primitive then we can just transform it using a simple function
(val: T[K]) => T[K]
: // if it's an object then it should either be a formatter schema or a transformer function
FormatterSchema<T[K]> | ((val: T[K]) => T[K])
}
An Interpreter
The implementation of an interpreter that satisfies the above can done as follows:
type Formatter<T> = (val: T) => T
const isFormatterFn = <T>(
formatter: FormatterSchema<T>[keyof T]
): formatter is Formatter<T[keyof T]> => typeof formatter === 'function'
const isFormatterObj = <T>(
formatter: FormatterSchema<T>[keyof T]
): formatter is Exclude<Formatter<T[keyof T]>, Function> =>
typeof formatter === 'object'
export const interpret =
<T>(schema: FormatterSchema<T>): Formatter<T> =>
(val) => {
const keys = Object.keys(schema) as Array<keyof T>
return keys.reduce<T>((prev, key) => {
const keySchema = schema[key]
const keyVal = val[key]
const isSchemaFn = isFormatterFn(keySchema)
if (isSchemaFn) {
return {
...prev,
[key]: keySchema(keyVal),
}
}
const isSchemaObj = isFormatterObj(keySchema)
if (isSchemaObj) {
return {
...prev,
[key]: interpret(keySchema)(keyVal),
}
}
return prev
}, val)
}
Transformers
We can define some general transformers, for example:
const trunc = Math.trunc
const round = Math.round
const length = (len: number) => (val: string) => val.slice(0, len)
const trim = (val: string) => val.trim()
const constant =
<T>(val: T) =>
() =>
val
const optional =
<T>(fn: (val: T) => T) =>
(val?: T) =>
val === undefined ? undefined : fn(val)
export const formatter = {
trunc,
round,
length,
trim,
constant,
optional,
}
Usage
For the sake of example we can define a data type to use
type User = {
name: string
location: {
address: string
city: string
gps?: [number, number]
}
}
const user: User = {
name: 'Bob Smithysmithson',
location: {
city: 'Somewhere secret',
address: '123 Secret street',
gps: [123, 456],
},
}
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
const format = interpret<User>({
name: formatter.length(10),
location: {
city: (val) => (['Home', 'Away'].includes(val) ? val : 'Other'),
gps: (val) => (isValidGPS(val) ? val : undefined),
},
})
const result = format(user)
console.log(result)
// {
// name: 'Bob Smithy',
// location: {
// city: 'Other',
// adddtess: '123 Secret street',
// gps: undefined,
// }
// }
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
const validate = interpret<User>({
name: (val) => {
if (val.length > 5) {
throw 'Length too long'
}
return val
},
})
// throws with Length too long
validate({
name: 'Jack Smith',
})
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