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

1
export type Primitive = string | number | boolean | Symbol | Date
2
3
/** * Define a formatter that can take in a generic object type and format each entry of that object */
4
export type FormatterSchema<T> = {
5
[K in keyof T]?: T[K] extends Primitive
6
? // if the value is primitive then we can just transform it using a simple function
7
(val: T[K]) => T[K]
8
: // if it's an object then it should either be a formatter schema or a transformer function
9
FormatterSchema<T[K]> | ((val: T[K]) => T[K])
10
}

An Interpreter

The implementation of an interpreter that satisfies the above can done as follows:

1
type Formatter<T> = (val: T) => T
2
const isFormatterFn = <T>(
3
formatter: FormatterSchema<T>[keyof T]
4
): formatter is Formatter<T[keyof T]> => typeof formatter === 'function'
5
6
const isFormatterObj = <T>(
7
formatter: FormatterSchema<T>[keyof T]
8
): formatter is Exclude<Formatter<T[keyof T]>, Function> =>
9
typeof formatter === 'object'
10
11
export 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 prev
36
}, val)
37
}

Transformers

We can define some general transformers, for example:

1
const trunc = Math.trunc
2
3
const round = Math.round
4
5
const length = (len: number) => (val: string) => val.slice(0, len)
6
7
const trim = (val: string) => val.trim()
8
9
const constant =
10
<T>(val: T) =>
11
() =>
12
val
13
14
const optional =
15
<T>(fn: (val: T) => T) =>
16
(val?: T) =>
17
val === undefined ? undefined : fn(val)
18
19
export 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

1
type User = {
2
name: string
3
location: {
4
address: string
5
city: string
6
gps?: [number, number]
7
}
8
}
9
10
const 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

1
const 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
9
const result = format(user)
10
11
console.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

1
const validate = interpret<User>({
2
name: (val) => {
3
if (val.length > 5) {
4
throw 'Length too long'
5
}
6
7
return val
8
},
9
})
10
11
// throws with Length too long
12
validate({
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