TypeManager is a stable, highly-tested, highly-performant parsing package for TypeScript which handles everything you need to easily integrate classes into your workflow.
Most frontend developers are writing custom helpers and functions (in the form of React Hooks and Vue Composables) to solve a problem that, in the backend world, was solved elegantly decades ago by the use of Classes.
Imagine handling a User object that contains a first name, last name, and nickname. You need to intelligently display the best name for that user in multiple places.
// Hi! I'm some user data from the server!
const user = fetchUserData();
In the world of TypeScript, it has become common to see something like this, repeated in every single file that needs this functionality:
import { useFormattingFunctions } from '@/hooks/useFormattingFunctions';
// Get name formatting function.
const { nameFormatter } = useFormattingFunctions();
// A user in the form of JS object.
const preferredName = nameFormatter(user); // ...finally got what I needed!
But in the backend world, this sort of problem was solved decades ago:
// A user in the form of `new MyUserClass()`.
const preferredName = user.getPreferredName(); // Whew! That was quick!
The problem is that, when you're used to working with plain-old JavaScript objects, you're used to having data (only!) and no business logic. But ask yourself: which one is simpler? Which one is easier to read?
TypeManager will help you to transform JSON strings or plain objects directly into class instances. No more need for unsafe JSON.parse and JSON.stringify functions. Forget about manual mapping and limitations. We support data transformations, circular references, naming conventions, generic and polymorphic types. Parsing was never so fun and easy.
Configuration can be done using decorators or declarative configuration for your or 3rd party classes.
It almost couldn't be easier. Instead of making a file like this:
// The old way...
export interface User
{
name: string;
email: string;
}
Just add a few extra keywords:
// The new way!
import { Type, Property } from '@dipscope/type-manager';
@Type()
export class User
{
@Property(String) public name: string;
@Property(String) public email: string;
}
Then, in the place where you used to get your data:
// The old way...
const users = await fetchUsers();
...You parse them instead, using TypeManager's deserialize() method:
// The new way!
import { TypeManager } from '@dipscope/type-manager';
const users = TypeManager.deserialize(User, await fetchUsers());
And if you need to send them back to the server, you can just go the other direction using serialize():
// The new way!
import { TypeManager } from '@dipscope/type-manager';
const rawUserData = TypeManager.serialize(User, user);
const response = http.put('/users/123', rawUserData)
You're darn right it's that easy! Now we can use all the power provided by JavaScript class instances without worriyng about the annoying stuff.
Furthermore TypeManager.TS provides you:
Want to know more? Let's dive into the details.
TypeManager.TS is available from NPM, both for browser (e.g. using webpack) and NodeJS:
npm i @dipscope/type-manager
TypeScript needs to run with the experimentalDecorators and emitDecoratorMetadata options enabled when using decorator annotations. So make sure you have them properly enabled in your tsconfig.json file.
If you want additional type-safety and reduced syntax you may wish to install reflect-metadata. This step is your choice and fully optional. All you have to do is make sure it's available globally to work -- this can usually be done with import 'reflect-metadata'; in your main index file.
Starting from TypeScript 5 we will also support the modern decorator syntax. However, parameter decorations with this modern syntax are not supported - you will not be able to use the Inject decorator provided by the library as well as reflect-metadata package if you're using modern syntax. We will add support as soon as it's provided by TypeScript, however. In the meantime, if you need the Inject decorator and enabling legacy decorators support is not an option - you can simply use the declarative configuration style.
If you like or are using this project, please give it a star. Thanks!
Let's have a look at that simple example of configuration using decorators from above.
import { Type, Property } from '@dipscope/type-manager';
@Type()
export class User
{
@Property(String) public name: string;
@Property(String) public email: string;
}
Here we have a User class with Type and Property decorators assigned to it. Type decorator declares a type. Property decorator describes available properties for that type.
You can write the same configuration can using our "declarative style".
import { TypeManager, TypeMetadata, TypeConfiguration } from '@dipscope/type-manager';
export class User
{
public name: string;
public email: string;
}
export class UserConfiguration implements TypeConfiguration<User>
{
public configure(typeMetadata: TypeMetadata<User>): void
{
typeMetadata.configurePropertyMetadata('name')
.hasTypeArgument(String);
typeMetadata.configurePropertyMetadata('email')
.hasTypeArgument(String);
return;
}
}
TypeManager.applyTypeConfiguration(User, new UserConfiguration());
As you can see now our User class has been defined without decorators. Instead, you call the TypeManager method and provide TypeConfiguration related to the User type.
No matter what style of configuration you have chosen, the next step is to call the serialize and deserialize methods of TypeManager, providing a type and the data you want to process.
import { TypeManager } from '@dipscope/type-manager';
const userObject = TypeManager.serialize(User, new User());
const user = TypeManager.deserialize(User, userObject);
user instanceof User; // True.
Calling serialize() creates a plain object, while deserialize creates an instance of the User class. During the deserialization process, you can provide any object. It's not necessary that the object was produced by TypeManager.
If the object is an Array, you will get array of types in return. Objects are parsed based on general type configuration defined by the developer. It is also possible to stringify and parse JSON.
import { TypeManager } from '@dipscope/type-manager';
const userJson = TypeManager.stringify(User, new User());
const user = TypeManager.parse(User, userJson);
user instanceof User; // True.
The stringify() and parse() functions are wrappers over native JSON class functions. In addition, they add serialize and deserialize support under the hood.
Static functions are not the only way to work with a TypeManager. You can also work in an instance-based manner.
import { TypeManager } from '@dipscope/type-manager';
const userManager = new TypeManager();
const userObject = userManager.serialize(User, new User());
const user = userManager.deserialize(User, userObject);
user instanceof User; // True.
At first glance, it may seem that there is no difference; however, creating an instance of TypeManager preserves a configuration state.
You can work with different configurations at the same time and have different serialization groups. By default, all decorator-based configurations and static calls are applied
to the singleton TypeManager instance which is automatically created under the hood.
There are few decorators which control the main flow. These are the Type, Property and Inject decorators. Let's go through each of them.
Type decorator defines a type and should be declared right before a class.
import { Type } from '@dipscope/type-manager';
@Type()
export class User
{
...
}
This will register a new type with default type serializer assigned to it. You can define how each class should be treated by providing optional configure options as a first argument.
import { Type } from '@dipscope/type-manager';
@Type({
alias: 'User',
serializer: new UserSerializer()
})
export class User
{
...
}
This call defines a type alias which can be later used to resolve a type for a property at runtime. We will talk about details in the property decorator section. Also we defined custom serializer for a type which is an implementation of Serializer interface. This serializer will be used later to serialize and deserialize a type including all custom logic of your choice. You can read more about creating a custom serializer in a separate section.
There are more options can be provided for a type, so check TypeOptions definition or section with decorator options below.
Property decorator defines per property configuration within a type and should be declared right before a property or accessor definition.
import 'reflect-metadata';
import { Type, Property } from '@dipscope/type-manager';
@Type()
export class User
{
@Property() public name: string;
}
This will register a name property for a User. Each property has a type associated with it. In our case this is a String. By default if no configure options are provided decorator will try to resolve a property type using reflect-metadata. If you are not using reflect metadata then such configuration will result a property type to be unknown and it will result in direct serialization. For such a case you have to explicitly define a
$ claude mcp add TypeManager.TS \
-- python -m otcore.mcp_server <graph>