metatyper
v1.0.10
Published
The MetaTyper project provides a powerful approach to using runtime types in TypeScript and JavaScript code
Downloads
10
Maintainers
Readme
code faster, smarter, better
Introduction
The MetaTyper project provides a powerful approach to using runtime types in TypeScript and JavaScript code.
It is based on the principle of using classical objects or classes as a data schema for further validation and serialization. The goal of the project is to make runtime types as developer-friendly as possible.
More Facts:
- Works in Node.JS and all modern browsers.
- Automatically infers TypeScript types.
- Works with native JavaScript.
- It's tiny.
- Zero dependencies.
- Rich error details.
- Rich extensibility support.
Installation
npm install metatyper
or
yarn add metatyper
or
<script src="https://cdn.jsdelivr.net/npm/metatyper/lib/metatyper.min.js"></script>
<!-- or another cdn -->
<script src="https://www.unpkg.com/metatyper/lib/metatyper.min.js"></script>
In this case, you need to use this library through the
MetaTyper
global variable:MetaTyper.Meta({ /* ... */ })
Basic Usage
First, you can create a Meta object.
import { Meta, NUMBER } from 'metatyper'
const user = Meta({
id: 0,
username: 'some user name',
stars: NUMBER({ min: 0, default: 0 })
})
user.id = 'some text' // type & validation error
user.stars = 'some text' // type & validation error
You can also simply validate different objects.
import { BOOLEAN, INTEGER, Meta, STRING } from 'metatyper'
const userSchema = {
id: INTEGER(),
username: STRING({
minLength: 3,
regexp: '^[a-zA-Z0-9 _]+$'
}),
stars: 0, // any number
someFlag: BOOLEAN({ optional: true })
}
Meta.validate(userSchema, {
id: 1.1, // throw a validation error
username: 'some user name',
stars: 1
})
Meta.validate(userSchema, {
id: 1,
username: 'some user name'
// throw a validation error, because stars is not optional field
})
Finally, you can work with classes instead of objects.
import { DATE, INTEGER, Meta, STRING } from 'metatyper'
import 'reflect-metadata'
@Meta.Class({ ignoreProps: ['someInstanceFlag', 'someClassFlag'] })
class User {
@Meta.declare(INTEGER({ default: 0 }))
id: number
username = 'some user name'
@Meta.declare({ default: 0, min: 0 })
stars: number // for this you need `reflect-metadata`
createdAt = DATE({
default: new Date(),
coercion: true // cast a string or number to a date (and vice versa)
})
someInstanceFlag = false
static someClassFlag = true
static someAnotherClassFlag = true
}
User.someClassFlag = 'string' as any
// ok, because the field is listed in the ignoreProps
User.someAnotherClassFlag = 'string' // type & validation error
const user = new User()
Meta.deserialize(user, {
id: 2,
username: 'some another user name',
createdAt: 1704067200 * 1000,
// this timestamp will cast to Date("2024-01-01")
someInstanceFlag: 'no boolean'
// ok, because the field is listed in the ignoreProps
})
Meta.deserialize(user, {
id: 1.1 // validation error
})
Table of Contents
- Introduction
- Installation
- Basic Usage
- Table of Contents
- Documentation
- Similar Libraries
- Change Log
Documentation
Meta Objects
Meta
To work with Meta types it is convenient to use Meta objects. A Meta object is a proxy object that changes the logic of reading and writing values to the object's properties.
Example:
const objA = {
a: 1
}
const metaObjA = Meta(objA)
// throw a validation error because this property has been initialized number
metaObjA.a = 'str'
const metaObjB = Meta()
metaObjB.a = 'str'
// throw a validation error because this property has been initialized string
metaObjB.a = 2
Since classes are more often used to describe properties, this library provides Meta classes. The difference from Meta objects is that instances of the class will also be Meta objects.
Example:
class A {
a = 'string'
static staticA = 2
}
const MetaA = Meta(A) // similar to the @Meta.Class() decorator
// throw a validation error because this property was initialized number
MetaA.staticA = 'str' as any
const metaInstanceA = new MetaA()
// throw a validation error because this property was initialized 'string'
metaInstanceA.a = 1 as any
To get an original object from Meta object you can use
Meta.proto(metaObject)
method.
Meta args
This is arguments for creating a Meta object.
function Meta<T extends object>(protoObject?: T, metaArgs?: MetaArgsType): Meta<T>
Meta function has the following arguments:
type MetaArgsType = {
name?: string
initialValues?: Record<string | symbol, any>
ignoreProps?: (string | symbol)[] | ((propName: string | symbol) => boolean)
validationIsActive?: boolean
serializationIsActive?: boolean
changeHandlers?: MetaChangeHandlerInfoType[]
errorHandlers?: MetaErrorHandlerInfoType[]
metaTypesArgs?: MetaTypeArgsType | ((metaTypeImpl: MetaTypeImpl) => MetaTypeArgsType)
metaTypesResolver?: MetaTypesResolver
autoResolveMetaTypes?: boolean
dynamicDeclarations?: boolean
metaInstanceArgs?: MetaArgsType | 'same'
buildMetaInstance?: boolean
metaBuilder?: MetaObjectsBuilder
} & Record<string, any>
name?: string
- A string that overrides the default name of the Meta object. The name is used when displaying the Meta object. For example, if the default name isMetaObject
, you can passMyMetaObject
as the name argument to change it.
initialValues?: Record<string | symbol, any>
- An object that defines the initial values of the properties of the Meta object. Thedefault
value is{}
.
ignoreProps?: (string | symbol)[] | ((propName: string | symbol) => boolean)
- Specifies which properties of the Meta object should be ignored by the Meta object. Thedefault
value is[]
. It can be either:- An array of strings or symbols that represent the property names to ignore.
- A function that takes a property name as an argument and returns a boolean value indicating whether to ignore it or not.
validationIsActive?: boolean
- A boolean that indicates whether the Meta object should perform validation on the Meta object or not. Thedefault
value istrue
.
serializationIsActive?: boolean
- A boolean that indicates whether the Meta object should perform serialization on the Meta object or not. Thedefault
value istrue
.
changeHandlers?: MetaChangeHandlerInfoType[]
- An array of handlers that handle changes in the Meta object. Thedefault
value is[]
.
errorHandlers?: MetaErrorHandlerInfoType[]
- An array of handlers that handle errors in the Meta object. Thedefault
value is[]
.
metaTypesArgs?: MetaTypeArgsType | ((metaTypeImpl: MetaTypeImpl) => MetaTypeArgsType)
- Defines the arguments for building or rebuilding the Meta types of the Meta object. It can be either:- An object that contains the properties and values of the Meta types arguments.
- A function that takes a Meta type implementation as an argument and returns an object of Meta types arguments.
The
default
value is{}
.
metaTypesResolver?: MetaTypesResolver
- A function that resolves Meta types from values. It takes any value as an argument and returns a Meta type:(value: any, args?: MetaTypeArgsType) => MetaTypeImpl
. Thedefault
value isMetaTypeImpl.getMetaTypeImpl
autoResolveMetaTypes?: boolean
- A boolean that indicates whether the Meta object should automatically resolve the Meta types from a value or not. For example, the value 1 in theMeta({field: 1 })
object will be used to declare aNUMBER
metatype. Thedefault
value istrue
.
dynamicDeclarations?: boolean
- A boolean that indicates whether the Meta object should allow new declarations of new properties or not. For example, you can define a new metatype like this:metaObject.anyField = NUMBER({ default: 1 })
. Thedefault
value istrue
.
metaInstanceArgs?: MetaArgsType | 'same'
- Defines the arguments for creating the Meta instance of the Meta class. It can be either:- An object that contains the properties and values of the Meta instance arguments.
- The string
'same'
that indicates that the same arguments as the Meta class should be used. Thedefault
value is'same'
.
buildMetaInstance?: boolean
- A boolean that indicates whether the Meta class should build the Meta instance or not. If true, the Meta class will use the metaInstanceArgs object to create the Meta instance. Thedefault
value istrue
.
metaBuilder?: MetaObjectsBuilder
- A Meta objects builder that is used to create the Meta object. The Meta objects builder is an object that implements the MetaObjectsBuilder interface. Thedefault
value is the global Meta objects builder (MetaObjectsBuilder.instance
).
Record<string, any> - used if you want to use a custom builder
To get the Meta args of a Meta object you can use
Meta.getMetaArgs(metaObject)
method.
Meta inheritance
Meta objects support the extension of classic objects with an additional side effect.
Objects inheritance
import { BOOLEAN, Meta, MetaType, NUMBER, STRING } from 'metatyper'
const obj1: any = {
a: 1,
b: NUMBER({ optional: true })
}
const obj2: any = {
c: 2,
d: STRING({ optional: true })
}
const obj3: any = {
e: 3,
f: BOOLEAN({ optional: true })
}
Object.setPrototypeOf(obj2, obj1)
const metaObj2 = Meta(obj2)
// true,
// because obj1 is not a Meta object
// and there is no special logic for its properties
console.log(metaObj2.b instanceof MetaType)
// but this will create its own MetaObj2 property 'a'
// and add a Meta type declaration NUMBER({ default: 0 })
metaObj2.b = NUMBER({ default: 0 })
console.log(metaObj2.b === 0) // true
Object.setPrototypeOf(obj3, metaObj2)
obj3.e = '1' // ok, because obj3 is not a Meta object
// validation error,
// because this is a property of the MetaObj2 Meta object
// and there is special logic for it
obj3.c = '1'
The prototype of a Meta object is the original object:
Object.getPrototypeOf(metaObj2) === obj2
Classes inheritance
import { Meta, NUMBER } from 'metatyper'
@Meta.Class()
class A {
static a = NUMBER({ optional: true })
a = NUMBER({ optional: true })
}
class B extends A {
static b = NUMBER({ optional: true })
b = NUMBER({ optional: true })
}
@Meta.Class()
class C extends B {
static c = NUMBER({ optional: true })
c = NUMBER({ optional: true })
}
console.log(A.toString())
// [<meta> class A] { a: NUMBER = undefined }
console.log(B.toString())
// [<meta child> class B] { b = NUMBER }
console.log(C.toString())
// [<meta> class C] { c: NUMBER = undefined; [a]: NUMBER = undefined }
// brackets [a] mean that property is a property of the parent Meta class
const aInstance = new A()
const bInstance = new B()
const cInstance = new C()
console.log(aInstance.toString())
// [<meta> instance A] { a: NUMBER = undefined }
console.log(bInstance.toString())
// [<meta> object] { a: NUMBER = undefined; b: NUMBER = undefined }
console.log(cInstance.toString())
// [<meta> instance C] { a: NUMBER = undefined; b: NUMBER = undefined; c: NUMBER = undefined }
// There are no [a] brackets in instances,
// as these properties are intrinsic
// (you can learn how js instance creation works)
Static classes work as simple Meta objects.
Meta.Class decorator
Decorator does the same thing as Meta(A)
.
Example:
import { Meta } from 'metatyper'
@Meta.Class() // Meta.Class(args) has arguments as in Meta({}, args)
class MetaA {
a = 'string'
static a = 2
}
// throw a validation error because this property was initialized number
MetaA.a = 'str'
const metaInstanceA = new MetaA()
// throw a validation error because this property was initialized 'string'
metaInstanceA.a = 1
Meta.declare decorator
This decorator lets you specify the Meta type of your properties.
You can do this in different ways:
- Specify the Meta type explicitly:
class Test {
@Meta.declare(NUMBER({ min: 0 }))
a: number
}
- Let the decorator infer the Meta type from the property value.
class Test {
@Meta.declare({ min: 0 })
a: number = 0
}
- Use
reflect-metadata
to automatically resolve the Meta type from the property type:
class Test {
@Meta.declare({ min: 0 })
a: number
}
You need to import
reflect-metadata
before using the option. Otherwise, the Meta type will beANY()
.
Meta.isMetaObject
If you need to check if an object is a Meta object, you can use this method: Meta.isMetaObject(obj)
.
Meta.isIgnoredProp
If you need to check if an property is ignored by Meta, you can use this method: Meta.isIgnoredProp(obj, 'propName')
.
Meta.copy
Sometimes you may need to copy a Meta object. You can do this by using the spread operator: { ...metaObject }
. However, this will not copy the type declarations of the Meta object. To copy the type declarations as well, you can use Meta.copy
:
const metaObjectCopy = Meta.copy(metaObject)
This method creates a copy of a Meta object and preserves its values, types, prototype and arguments.
Example:
import { Meta, STRING } from 'metatyper'
const origObject: any = { a: 1 }
const origMetaObject = Meta(origObject)
origMetaObject.a = 2
origMetaObject.b = STRING({ default: '' })
const metaObjectCopy = Meta.copy(origMetaObject)
metaObjectCopy.a === 2 // true
metaObjectCopy.b === '' // true
Meta.rebuild
You may also need to reset the meta object to its original state.
The Meta.rebuild
is useful for creating a new instance of a Meta object with its initial state and configuration.
const newMetaObject = Meta.rebuild(metaObject)
This method rebuilds a Meta object using the same original object and arguments that were used to create the Meta object.
Example:
import { Meta, STRING } from 'metatyper'
const origObject: any = { a: 1 }
const origMetaObject = Meta(origObject)
origMetaObject.a = 2
origMetaObject.b = STRING({ default: '' })
const newMetaObject = Meta.rebuild(origMetaObject)
newMetaObject.a === 1 // true
newMetaObject.b === undefined // true
// because `origObject` was used to create the new Meta object
Meta Types
MetaType
Meta types extend built-in types, but they have more features: validation and serialization. The basic logic of Meta types is in metaTypeImpl.
Example, how to create a new Meta type:
import { MetaType, StringImpl } from 'metatyper'
const newType1 = MetaType<string>(StringImpl, {
/* metaTypeArgs */
})
const newType2 = MetaType<string>(
StringImpl.build({
/* metaTypeArgs */
})
)
MetaType Implementation
Meta type implementation example:
import { MetaType, StringImpl } from 'metatyper'
class LowerCaseStringImpl extends StringImpl {
static isCompatible(value: string) {
if (!this.isCompatible(value)) {
return false
}
return !/[A-Z]/.test(value)
}
}
export function LowerCaseString() {
return MetaType<LowerCaseString>(LowerCaseStringImpl)
}
type LowerCaseString = MetaType<Lowercase<string>, LowerCaseStringImpl>
import { Meta } from 'metatyper'
@Meta.Class()
class MyNewExample {
str = LowerCaseString()
}
const instance = new MyNewExample()
instance.str = 'abc' // ok
instance.str = 'aBc' // type and validation error
To learn more about the principles of Meta types creation, you can explore the source code of the built-in Meta types.
MetaTypeArgsType
This represents the arguments for creating a Meta type.
type MetaTypeArgsType<
T = any,
IsNullishT extends boolean = boolean,
IsNullableT extends boolean = IsNullishT,
IsOptionalT extends boolean = IsNullishT
> = {
name?: string
subType?: any
default?: T | ((declaration?: MetaTypeImpl) => T)
nullish?: IsNullishT
nullable?: IsNullableT
optional?: IsOptionalT
coercion?: boolean
validateType?: boolean
noBuiltinValidators?: boolean
noBuiltinSerializers?: boolean
noBuiltinDeSerializers?: boolean
validators?: (ValidatorType | ValidatorFuncType)[]
serializers?: (SerializerType | SerializeFuncType)[]
deserializers?: (DeSerializerType | DeSerializeFuncType)[]
} & Record<string, any>
name?: string
- A string that overrides the default name of the Meta type. The name is used when displaying the Meta type.
subType?: any
- A Meta type or a value that defines the type of the nested values in the value.
For example, if the value is an array, you can use the subType to specify the type of the elements in the array.
default?: T | ((declaration?: MetaTypeImpl) => T)
- A value or a function that returns a value that is used as the default value for the Meta type.
The default value is used when the initial value is undefined
.
nullish?: boolean
- A boolean indicating whether the value can be null
or undefined
.
If false
, a NullableValidator
and an OptionalValidator
are added to the Meta type. The default value is false
.
nullable?: boolean
- A boolean indicating whether the value can be null
.
If false
, a NullableValidator
is added to the Meta type. If nullish
and nullable
are contradictory,
the value of nullable
will be chosen. Default value is the same as nullish
.
optional?: boolean
- A boolean indicating whether the value can be undefined
. If false
, an OptionalValidator
is added to the Meta type.
If nullish
and optional
are contradictory, the value of optional
will be chosen. Default value is the same as nullish
coercion?: boolean
- A boolean that indicates whether the value should be coerced to the expected type or not.
If true
, an CoercionSerializer
is added to the Meta type, which tries to convert the main value to the appropriate type.
For example, if the Meta type is a string, and the main value is a number, the number will be cast to a string.
validateType?: boolean
- A boolean that indicates whether the value should be validated against the expected type or not.
If true
, a MetaTypeValidator
is added to the Meta type, which checks that the main value matches the Meta type.
Default value is true
.
noBuiltinValidators?: boolean
- A boolean that indicates whether the built-in validators should be disabled or not.
If true
, the Meta type will not use any of the default validators, like MetaTypeValidator
or NullableValidator.
Default value is false
.
noBuiltinSerializers?: boolean
- A boolean that indicates whether the built-in serializers should be disabled or not.
If true
, the Meta type will not use any of the default serializers, like CoercionSerializer
.
Default value is false
.
noBuiltinDeSerializers?: boolean
- A boolean that indicates whether the built-in deserializers should be disabled or not.
If true
, the Meta type will not use any of the default deserializers, like CoercionSerializer
or ToLowerCaseSerializer
(case argument in STRING).
Default value is false
.
validators?: (ValidatorType | ValidatorFuncType)[]
- An array of validators that are used to check the value when it is assigned to an object property.
type ValidatorFuncType = (validateArgs: ValidatorArgsType) => boolean
type ValidatorType = {
name?: string
validate: ValidatorFuncType
}
You can read about validation and ValidatorArgsType in the following section: Validation
serializers?: (SerializerType | SerializeFuncType)[]
- An array of serializers that change the value when it is retrieved from the object.
For example, obj['prop']
or Meta.serialize(obj)
.
type SerializeFuncType = (serializeArgs: SerializerArgsType) => any
type SerializerType = {
serialize: SerializeFuncType
name?: string
serializePlaces?: ('get' | 'serialize' | 'unknown')[] | string[]
}
You can read about serialization and SerializerArgsType in the following section: Serialization and Deserialization
deserializers?: (DeSerializerType | DeSerializeFuncType)[]
- An array of deserializers that modify the value when it is set to an object property,
prior to validation. For example, obj['prop'] = 'value'
or Meta.deserialize(metaObject, rawObject)
.
type DeSerializeFuncType = (deserializeArgs: DeSerializerArgsType) => any
type DeSerializerType = {
serialize: DeSerializeFuncType
name?: string
deserializePlaces?: ('init' | 'reinit' | 'set' | 'deserialize' | 'unknown')[] | string[]
}
You can read about deserialization and DeSerializerArgsType in the following section: Serialization and Deserialization
Built-in Meta Types
Each built-in Meta type has args?: MetaTypeArgsType
at the end of arguments. How to use it you can see below.
ANY
import { ANY, Meta } from 'metatyper'
const obj1 = Meta({
a: ANY({ nullish: true })
}) // as { a: any }
obj1.a = 1
obj1.a = {}
BOOLEAN
import { BOOLEAN, Meta } from 'metatyper'
const obj = Meta({
someField: BOOLEAN({
default: false,
// BooleanMetaTypeArgs
trueValues: [1],
// will replace 1 with true
falseValues: [(value) => value === 0]
// will replace 0 with false
})
}) // as { someField: boolean }
obj.someField = true
obj.someField = 1 as boolean
obj.someField = 'true' // type & validation error
STRING
import { Meta, STRING } from 'metatyper'
const obj = Meta({
someField: STRING({
nullish: true,
// StringMetaTypeArgs
notEmpty: true, // == minLength: 0
minLength: 0,
maxLength: 10,
regexp: '^[a-zA-Z]+$',
// validate using this regular expression
toCase: 'lower'
// serialize to lowercase (or 'upper')
})
}) // as { someField?: string | null | undefined }
obj.someField = 'STR' // will serialize to lowercase
obj.someField = 1 // type & validation error
NUMBER
import { Meta, NUMBER } from 'metatyper'
const obj = Meta({
someField: NUMBER({
nullish: true,
// NumberMetaTypeArgs
min: 1, // value >= 1
max: 9, // value <= 9
greater: 0, // value > 0
less: 10 // value < 10
})
}) // as { someField?: number | null | undefined }
obj.someField = 1.2
obj.someField = 11 // validation error
obj.someField = 'str' // type & validation error
INTEGER
import { INTEGER, Meta } from 'metatyper'
const obj = Meta({
someField: INTEGER({
nullish: true,
// NumberMetaTypeArgs
min: 1, // value >= 1
max: 9, // value <= 9
greater: 0, // value > 0
less: 10 // value < 10
})
}) // as { someField?: number | null | undefined }
obj.someField = 1
obj.someField = 11 // validation error
obj.someField = 1.1 // validation error
BIGINT
import { BIGINT, Meta } from 'metatyper'
const obj = Meta({
someField: BIGINT({
nullish: true,
// NumberMetaTypeArgs
min: 1, // value >= 1
max: 9, // value <= 9
greater: 0, // value > 0
less: 10 // value < 10
})
}) // as { someField?: bigint | null | undefined }
obj.someField = 1n
obj.someField = 11n // validation error
obj.someField = 1 // type and validation error
DATE
import { DATE, Meta } from 'metatyper'
const obj = Meta({
someField: DATE({
nullish: true,
// DateMetaTypeArgs
min: 1, // value >= new Date(1)
max: new Date(), // value <= new Date()
greater: 0, // value > new Date(0)
less: 10n // value < new Date(10)
})
}) // as { someField?: Date | null | undefined }
obj.someField = new Date(1)
obj.someField = 1 // type and validation error
LITERAL
import { LITERAL, Meta } from 'metatyper'
const obj = Meta({
someField: LITERAL(1, {
nullish: true
})
}) // as { someField?: 1 | null | undefined }
obj.someField = 1
obj.someField = 2 // type and validation error
INSTANCE
import { Meta, INSTANCE } from 'metatyper'
class A {
a = 1
}
class B extends A {
b = 2
}
const obj = Meta({
someField: INSTANCE(B, {
nullish: true
// InstanceMetaTypeArgs
allowChildren: false
// disallow the use of children B, default: true
})
}) // as { someField?: B | null | undefined }
obj.someField = new B() // ok
obj.someField = new A() // validation error
obj.someField = {} // type and validation error
obj.someField = B // type and validation error
UNION
import { BOOLEAN, Meta, STRING, UNION } from 'metatyper'
const obj = Meta({
someField: UNION([BOOLEAN({ nullable: true }), STRING({ optional: true })])
})
// as { someField: (boolean | null) | (string | undefined) }
obj.someField = true // ok
obj.someField = new Date() // type and validation error
ARRAY
import { Meta, ARRAY, BOOLEAN, STRING } from 'metatyper'
const obj = Meta({
someField: ARRAY(
[
BOOLEAN({ default: null, nullable: true }),
STRING({ optional: true })
],
{
nullish: true,
// ArrayMetaTypeArgs
notEmpty: true, // == minLength: 0
minLength: 0,
maxLength: 10,
freeze: true,
// will create a frozen copy when deserializing
serializeSubValues: false // default: true
}
)
someField2: ARRAY(STRING(), { default: [] })
})
/*
as {
someField:
| readonly (boolean | null | string | undefined)[]
| null
| undefined
someField2: string[]
}
*/
obj.someField = ['1', '2'] // ok
obj.someField = [1, '1'] // type and validation error
TUPLE
import { Meta, STRING, TUPLE } from 'metatyper'
const obj = Meta({
someField: TUPLE([false, STRING({ optional: true })], {
nullish: true,
// TupleMetaTypeArgs
freeze: true,
// will create a frozen copy when deserializing
serializeSubValues: false // default: true
})
})
/*
as {
someField:
| readonly [ boolean, string | undefined ]
| null
| undefined
}
*/
obj.someField = [true, '1'] // ok
obj.someField = ['1', true] // type and validation error
OBJECT
import { Meta, OBJECT, STRING, BOOLEAN } from 'metatyper'
const obj = Meta({
someField: OBJECT({
a: 1,
b: 'string',
c: BOOLEAN(),
d: {
e: STRING({ optional: true }),
f: OBJECT({})
}
}, {
nullish: true,
// ObjectMetaTypeArgs
freeze: true,
// will create a frozen copy when deserializing
required: ['a', 'b', 'c'],
// by default all fields are required
serializeSubValues: false // default: true
})
someField2: OBJECT(BOOLEAN(), { optional: true })
})
/*
as {
someField: {
readonly a: number
readonly b: string
readonly c: boolean
readonly d?: {
e?: string
f: Record<string, any>
}
}
someField2?: Record<string, boolean>
}
*/
obj.someField = {
a: 2,
b: 'str',
c: false,
d: {
// e: 'optional field'
f: {
anyField: true
}
}
}
obj.someField = {
a: 2,
b: 'str',
// type and validation error, `c` is not an optional field
}
obj.someField2 = {
anyField: true
}
Recursive structures
Meta types like OBJECT
, ARRAY
, TUPLE
and UNION
inherited from StructuralMetaTypeImpl
.
So, it allows to create a recursive structures like this:
Use argument to create a REF
import { Meta, OBJECT } from 'metatyper'
OBJECT((selfImpl) => {
type MyObjectType = {
// ... any fields
self?: MyObjectType
}
return {
// ... any fields
self: selfImpl as any
} as MyObjectType
})
// OBJECT(g4dv1h)<{ self: REF<g4dv1h> }>
Be careful,
selfImpl
is of typeObjectImpl
Use a variable to create a REF
import { Meta, OBJECT } from 'metatyper'
type MyType = {
// ... any fields
self?: MyType
}
const myType: OBJECT<MyType> = OBJECT(() => {
return {
// ... any fields
self: myType
}
})
// OBJECT(g4dv1h)<{ self: REF<g4dv1h> }>
Use recursive structures to create a REF
import { Meta, OBJECT } from 'metatyper'
type MyType = {
// ... any fields
self?: MyType
}
const myTypeSchema: MyType = {
/* any fields */
}
myTypeSchema.self = myTypeSchema
OBJECT(myTypeSchema)
// OBJECT(g4dv1h)<{ self: REF<g4dv1h> }>
And of course you can create more complex structures like this
import { Meta, OBJECT, STRING, TUPLE } from 'metatyper'
const myObjectType = OBJECT((selfImpl) => {
type MyTuple = [MyObjectType, string, MyTuple]
type MyObjectType = {
a: number
b: { selfImpl: MyObjectType }
c: MyObjectType[]
d: MyTuple
}
const myObjectSchema: any = {
a: 1,
b: { selfImpl },
c: null,
d: TUPLE((selfImpl) => [myObjectType, STRING(), selfImpl])
}
myObjectSchema.c = [myObjectSchema, selfImpl, myObjectType]
return myObjectSchema as MyObjectType
})
console.log(myObjectType.toString())
/*
OBJECT(n6f76)<{
a: NUMBER,
b: OBJECT(jqrb3)<{ selfImpl: REF<n6f76> }>,
c: ARRAY(pugop)<
UNION(ljwth)<
REF<n6f76> | REF<n6f76> | REF<n6f76>
>[]
>,
d: TUPLE(zwgxz)<[ REF<n6f76>, STRING, REF<zwgxz> ]>
}>
*/
REF
- another optional type that works like a proxy.
Recursive value is not available now. You need to provide undefined
value instead of recursive reference.
e.g.
// OBJECT( (myObjImpl) => ({ myObj: myObjImpl }) )
const myObj = { myObj: { myObj: { myObj: undefined } } }
Validation
Meta objects come with built-in validation capability. Validators specific to each Meta type are utilized during the validation process. If validation fails, an exception is raised. For more information on validation errors, refer to the Errors section.
Validators for Meta types are categorized into:
Built-in Validators: For example,
STRING
Meta type usesMinLengthValidator
, configurable via theminLength
argument.Runtime Validators: These are provided as arguments at runtime to the Meta type.
For more details on the arguments accepted by Meta types, see the MetaTypeArgsType section.
Validation occurs automatically when assigning new values to a Meta object. Additionally, you can explicitly validate another object using the method:
Meta.validate(
metaObjectOrSchema: Meta<object> | object,
rawObject: object,
validateArgs?: {
safe?: boolean,
stopAtFirstError?: boolean
}
) => boolean
Example:
import { Meta } from 'metatyper'
const schema = {
id: 0,
name: STRING({
validators: [
/* validators here */
]
})
}
Meta.validate(schema, { id: '351', name: null })
// throws MetaTypeValidatorError
Validators
A validator is an object that contains a validate
method:
type ValidatorType = {
name?: string
validate: (args: ValidatorArgsType) => boolean
}
type ValidatorArgsType = {
value: any
metaTypeImpl?: MetaTypeImpl
propName?: string | symbol
targetObject?: object
baseObject?: object
safe?: boolean
stopAtFirstError?: boolean
} & Record<string, any>
value
: The value to be validated.metaTypeImpl
: The Meta type implementation invoking this validator.propName
: Specified when using Meta objects for validation.targetObject
: The object that needs to be validated.baseObject
: The object that contains the Meta type declaration with this validator.safe
: Determines whether a validation error should throw an exception.stopAtFirstError
: Specifies if validation should cease after the first error. Defaults to true.
Disabling Validation
Validation can be disabled using the methods below:
Method 1
Meta.validationIsActive(metaObj) === true
Meta.disableValidation(metaObj)
Meta.validationIsActive(metaObj) === false
metaObj.myProp = 0 // This is now allowed.
Meta.enableValidation(metaObj)
Method 2
@Meta.Class()
class MyClass2 {
myProp = MyType({
validators: [],
// Set empty array
noBuiltinValidators: true
// Disables the built-in validators like MetaTypeValidator or MinLengthValidator
})
}
Serialization and Deserialization
Serialization and deserialization of values are handled by Meta type's serializers and deserializers. Serialization is performed when retrieving a property’s value, whereas deserialization occurs during value assignment. Direct invocation of serialization and deserialization is also supported through specific methods:
Meta.serialize = (
metaObject: Meta<T> | T,
serializeArgs?: {
metaArgs?: MetaArgsType
}
): { [key in keyof T]: any }
You can specify the
serialize
result type:Meta.serialize<{ a: number }>(...)
Meta.deserialize = (
metaObjectOrProto: Meta<T> | T,
rawObject: object,
deserializeArgs?: {
metaArgs?: MetaArgsType
}
): Meta<T>
Example:
import { Meta } from 'metatyper'
const objToSerialize = { id: '351', date: new Date(123) }
const objToDeSerialize = Meta.serialize<{
id: string
date: number
}>({ id: '', name: DATE({ coercion: true }) }, objToSerialize)
// objToDeSerialize now equals { id: '351', date: 123 }
Meta.deserialize({ id: '', name: DATE({ coercion: true }) }, objToDeSerialize) // returns Meta({ id: '351', date: new Date(123) })
Serializers and Deserializers
A serializer is an object with a serialize
method, and a deserializer likewise has a deserialize
method:
type SerializerArgsType = {
value: any
metaTypeImpl?: MetaTypeImpl
propName?: string | symbol
targetObject?: object
baseObject?: object
place?: SerializePlaceType
}
type SerializerType = {
serialize: (serializeArgs: SerializerArgsType) => any
name?: string
serializePlaces?: SerializePlaceType[]
}
type DeSerializerArgsType = {
value: any
metaTypeImpl?: MetaTypeImpl
propName?: string | symbol
targetObject?: object
baseObject?: object
place?: DeSerializePlaceType
}
type DeSerializerType = {
deserialize: (deserializeArgs: DeSerializerArgsType) => any
name?: string
deserializePlaces?: DeSerializePlaceType[]
}
Each field's purpose in both serializers and deserializers closely aligns with those in validators, specifying the object context and invoking Meta type implementation.
Serialization and deserialization processes can be adjusted depending on their specific use cases with the help of SerializePlaceType
and DeSerializePlaceType
. This allows for a more precise control over how and where these processes occur. The term "place" refers to the specific scenario in which serialization occurs.
SerializePlaceType
indicates various contexts where serialization can happen:
get
: This is used when retrieving a property, for example, accessing a property likeobj.prop
.serialize
: This context is applied during the serialization of an object, such as when usingMeta.serialize(obj)
.unknown
: This default setting is used for custom serialization logic that does not fit the other predefined contexts.
Similarly, DeSerializePlaceType
outlines different scenarios for deserialization:
init
: Indicates the initialization of a new type declaration, like starting a Meta object withMeta({ prop: 'value' })
.reinit
: Used when re-initializing type declarations by defining a new Meta type, for example, changing a property's type withobj.prop = NUMBER()
.set
: Applies when setting a new property value, such asobj.prop = 1
.deserialize
: This context is for deserializing an object, done likeMeta.deserialize(obj, rawData)
.unknown
: The default setting for custom deserialization logic that doesn't align with the specified contexts.
Disable Serialization
To disable serialization or deserialization:
Method 1
Meta.serializationIsActive(metaObject) === true
Meta.disableSerialization(metaObject)
Meta.serializationIsActive(metaObject) === false
metaObject.myProp = 0 // Deserialization does not occur.
Meta.enableSerialization(metaObject)
Method 2
@Meta.Class()
class MyClass2 {
myProp = MyType({
serializers: [],
// Empty array disables serializers
deserializers: [],
// Empty array disables deserializers
noBuiltinSerializers: true,
// Disables all built-in serializers like Coercion
noBuiltinDeSerializers: true
// Disables all built-in deserializers like Coercion
})
}
Types Coercion
Meta types have coercion capabilities upon serialization and deserialization. This is particularly handy for handling various value types such as dates in JSON data.
undefined
andnull
values will not be converted
These built-in metatypes support coercion:
BOOLEAN coercion
BOOLEAN({ coercion: true })
Meta.deserialize
will cast value
to !!value
.
STRING coercion
STRING({ coercion: true })
Meta.deserialize
will cast any value
to string.
For the date value,
value.toISOString()
will be used
NUMBER coercion
NUMBER({ coercion: true })
Meta.deserialize
will cast value
depending on the type of the value:
Date
->value.getTime()
bigint
->Number(value)
string
->Number(value)
boolean
->Number(value)
INTEGER coercion
INTEGER({ coercion: true })
Meta.deserialize
will cast value
depends on the type of the value:
Date
->value.getTime()
bigint
->Number(value)
string
->Number(value)
boolean
->Number(value)
number
->Math.trunc(value)
BIGINT coercion
BIGINT({ coercion: true })
Meta.deserialize
will cast value
depends on the type of the value:
Date
->BigInt(value.getTime())
string
->BigInt(value)
boolean
->BigInt(value)
number
->BigInt(Math.trunc(value))
Meta.serialize
will cast value
to string
.
DATE coercion
DATE({ coercion: true })
Meta.deserialize
will cast value
depends on the type of the value:
bigint
->new Date(Number(value))
number
->new Date(value)
string
->new Date(value)
Meta.serialize
will cast value
to timestamp (value.getTime()
).
Errors
When working with Meta objects, you may encounter a range of errors. These can be broadly categorized into standard errors, such as TypeError('Cannot assign to read only property ...')
, and specialized errors unique to the handling of Meta objects. Understanding these errors and their hierarchy is crucial for effectively managing exceptions and maintaining robust code.
Specialized errors fall under the umbrella of MetaError
and include:
MetaTypeSerializationError
MetaTypeSerializationError
serves as an extended version of MetaError
, focusing specifically on the identification and handling of serialization errors. Its main purpose is to allow developers to pinpoint serialization issues distinctively using instanceof
checks. This differentiation is crucial for separating serialization errors from others, like validation errors, enhancing debugging efficiency.
MetaTypeSerializerError
When it comes to serialization of Meta type data, encountering errors is a possibility. The MetaTypeSerializerError
is designed to throw an exception in such cases. This error aims to simplify the debugging process and error handling by offering in-depth information about where and why the failure occurred.
Key fields of the MetaTypeSerializerError
:
serializer: This property provides a direct link to the
SerializerType
instance responsible for the error. This allows developers to easily identify which serializer was involved in the process and potentially inspect its configuration or state at the time of failure.serializerErrorArgs: Holding the type
SerializerErrorArgsType
, this property delivers a detailed look at the arguments fed into the serialization function at the error's occurrence. These arguments cover a range of information, from the value being serialized, the property's name, the target object, to additional options affecting serialization. WithinserializerErrorArgs
, there's asubError
field holding anError
instance, shedding light on the precise cause of the serialization failure. This layered error reporting strategy significantly aids in debugging by providing a clear context of the error beyond merely indicating its occurrence.
type SerializerErrorArgsType = {
value: any
subError?: Error
propName?: string | symbol
targetObject?: object
baseObject?: object
place?: SerializePlaceType
metaTypeImpl?: MetaTypeImpl
} & Record<string, any>
MetaTypeDeSerializerError
Mirroring the MetaTypeSerializerError
, the MetaTypeDeSerializerError
addresses errors during the deserialization of Meta type data. This exception is crucial for developers aiming to resolve issues arising from converting serialized data back into a usable form within the application.
Key fields of the MetaTypeDeSerializerError
:
deserializer: Reflecting the
serializer
attribute inMetaTypeSerializerError
, this field links to the deserializer instance that encountered the error, facilitating an understanding of which deserialization logic didn't succeed.deserializerErrorArgs: As
DeSerializerErrorArgsType
, this attribute documents the arguments present at the deserialization function during the error event. Providing a comprehensive context, these arguments include the value being deserialized, relevant property names, and the objects involved, among others.
type DeSerializerErrorArgsType = {
value: any
subError?: Error
propName?: string | symbol
targetObject?: object
baseObject?: object
place?: DeSerializePlaceType
metaTypeImpl?: MetaTypeImpl
} & Record<string, any>
MetaTypeValidationError
MetaTypeValidationError
functions as an enhanced variant of MetaError
, specifically dedicated to identifying and managing validation errors. Its primary role is to enable developers to efficiently distinguish validation errors from other types, such as serialization or deserialization errors, through instanceof
checks. This specificity is vital for streamlined error handling and debugging processes.
MetaTypeValidatorError
During the validation process of Meta type data, it's plausible to encounter failures. The MetaTypeValidatorError
is thrown in such scenarios to indicate a validation issue. The uniqueness of this error, compared to MetaTypeValidatorsArrayError
, is determined by the function arguments during validation. Specifically, setting stopAtFirstError: false
in the function arguments collects all encountered validation errors, encapsulating them within a MetaTypeValidatorsArrayError
containing multiple MetaTypeValidatorError
instances.
Key fields of the MetaTypeValidatorError
:
validator: This field connects directly to the
ValidatorType
that generated the error, allowing developers to identify and investigate the specific validator causing the issue.validatorErrorArgs: Holding the
ValidatorErrorArgsType
, this attribute provides a detailed account of the circumstances leading to the validation error. Information includes the value under validation, property names, the involved objects, and optionally, a subError detailing the underlying cause of failure, if applicable. Additionally, it may indicate whether the validation was set to stop at the first error or continue to gather all errors.
type ValidatorErrorArgsType = {
value: any
subError?: Error
propName?: string | symbol
targetObject?: object
baseObject?: object
metaTypeImpl?: MetaTypeImpl
stopAtFirstError?: boolean
} & Record<string, any>
MetaTypeValidatorsArrayError
Mirroring scenarios in which MetaTypeValidatorError
arises, the MetaTypeValidatorsArrayError
is thrown when multiple validation errors are encountered during the checking of Meta type data. This exception is particularly generated when the validation function’s argument stopAtFirstError
is set to false
, allowing the accumulation of all validation errors into a single MetaTypeValidatorsArrayError
. This error then contains an array of MetaTypeValidatorError
instances, providing a comprehensive overview of all validation issues detected.
Key fields of the MetaTypeValidatorsArrayError
:
- validatorsErrors: This attribute is an array of
MetaTypeValidatorError
instances, each detailing a specific validation failure encountered during the process. This collection offers a broad perspective on the nature and extent of validation issues, facilitating thorough error resolution.
Similar Libraries
There are many other popular good libraries that perform similar functions well. If you require specific functionality, you can explore various validation libraries (eg Zod) or type collections (eg Type-fest). Moreover, you have the flexibility to integrate the best features from these libraries alongside MetaTyper to achieve best outcomes.
These libs are worth a look:
- class-validator
- io-ts
- joi
- ow
- runtypes
- ts-toolbelt
- type-fest
- yup
- zod
Change Log
Stay updated with the latest changes and improvements: GitHub Releases.