A Type Generation Library for building exportable backend type definition, easily to adopt in different TypeScript based backend development
Description
MetaMatter is a library for building runtime type definition generation. It is built with TypeScript and combines the concepts of Meta-Programming
, to create type-safe
, scalable
environment for (mainly) Frontend development.
Philosophy
There are numbers of libraries for building a Node.js server-side applications. However, there is an issue when it comes to communicate between Frontend and Backend no matter which backend language we are using. Type Definitions
, would be troublesome when it comes to Agile
development style. Frequent updates on APIs makes it hard to develop projects in a type-safe
environment.
MetaMatter aims to address this issue, by providing a lightweight, less configuration tools.
Installation
> yarn add @metamatter/core
# or
> npm install @metamatter/core
Usage
There are three decorators at the moment.
@Property()
@Enum()
@Subclass()
@Property()
This is the basic decorator to tell the program to store the decorated field as part of the definition output.
Due to the limitation of Reflection in TypeScript,
union types
,interface
,enum
,Promise
,Array
are not working as expected, the implementation would not be as usual as it be.
Example
import { Property } from '@metamatter/core';
class User {
@Property()
username: string;
@Property({ type: String })
hobbies: string[];
@Property({ nullable: true })
phone?: string;
}
// Will produce the follow output:
const definition = {
name: 'User',
type: 'interface',
definition: '{username: string, hobbies: string[], phone: string | null}',
};
@Enum()
This decorator will work the same way as enum
does, but in JavaScript
, enum
will be translated to a frozen object, which cannot be able to apply any decorators, so we suggest keeping the enum in a class form
with static fields
.
Due to the limitation of Reflection in TypeScript,
union types
,interface
,enum
,Promise
,Array
are not working as expected, the implementation would not be as usual as it be.
Example
import { Enum } from '@metamatter/core';
@Enum()
class UserRole {
static readonly ADMIN = 'ADMIN';
static readonly USER = 'USER';
}
// Will produce the follow output:
const definition = {
name: 'UserRole',
type: 'enum',
definition: '{ADMIN: "ADMIN", USER: "USER"}',
};
@Subclass()
At some point that your application might grow in size, some class might share the same name but in different structure, in that way the program might generate two different interfaces with the same name, this is not what we want. That's why we need @Subclass decorator.
Since nested class cannot apply decorators as well, we need to declare it outside the class and assign it as a static property of another class
Example (Suggested)
import { Subclass, Property } from '@metamatter/core';
@Subclass()
class Wallet {
@Property()
balance: number;
}
class User {
static Wallet = Wallet;
@Property({ type: User.Wallet })
wallet: InstanceType<typeof User.Wallet>;
}
Example
import { Subclass, Property } from '@metamatter/core';
@Subclass()
class Wallet {
@Property()
balance: number;
}
class User {
@Property()
wallet: Wallet;
}
The above examples work the same way, but just in different style.
MetaMatter
This is the main class of the library, it provide two simple functions
getPropertyTree
generateTypeDefinition
MetaMatter.getPropertyTree
It accepts a class as parameter and it will return a complete Type
tree of all of it's properties.
Example
// What a Type look like
interface Type<T> {
type: string;
isPrimitive: boolean;
isArray: boolean;
isEnum: boolean;
isNullable: boolean;
isSubclass: boolean;
body: T extends object ? (object extends T ? T : { [P in keyof T]: Type<ExcludeArray<T[P]>> }) : null;
toString: (prefix?: string) => string;
toDefinition: (prefix?: string) => string;
}
import { MetaMatter, Property } from '@metamatter/core';
class User {
@Property()
name: string;
@Property()
age: number;
}
const tree = MetaMatter.getPropertyTree(User);
// Output
const tree = {
name: {
type: 'string',
isPrimitive: true,
isArray: false,
isEnum: false,
isNullable: false,
isSubclass: false,
body: null,
toString: () => 'string',
toDefinition: () => '';
},
age: {
type: 'number',
isPrimitive: true,
isArray: false,
isEnum: false,
isNullable: false,
isSubclass: false,
body: null,
toString: () => 'number',
toDefinition: () => '';
},
}
MetaMatter.generateTypeDefinition
It accepts a class as parameter and it will produce an array of Definition
.
Example
// What a Definition look like
interface Definition {
name: string;
type: 'interface' | 'enum';
definition: string;
}
import { MetaMatter, Property, Subclass } from '@metamatter/core';
@Subclass()
class Wallet {
@Property()
balance: number;
}
class User {
@Property()
name: string;
@Property()
age: number;
@Property({ nullable: true })
wallet?: Wallet;
}
const definitions = MetaMatter.generateTypeDefinition(User);
// Output
const definitions = [
{
name: 'User',
type: 'interface',
definition: '{name: string, age: number, wallet: User$Wallet | null}',
},
{
name: 'User$Wallet',
type: 'interface',
definition: '{balance: number}',
},
];