MCPcopy Index your code
hub / github.com/dipscope/TypeManager.TS

github.com/dipscope/TypeManager.TS @v8.3.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release v8.3.0 ↗ · + Follow
710 symbols 1,469 edges 196 files 381 documented · 54%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

TypeManager.TS

GitHub NPM Contributor Covenant

What is TypeManager?

TypeManager is a stable, highly-tested, highly-performant parsing package for TypeScript which handles everything you need to easily integrate classes into your workflow.

What does it do?

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 to the rescue

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.

What do I do?

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)

Wow! It's that easy?

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:

  • Reflection abilities at runtime.
  • Support for generic types.
  • Support for inheritance and polymorphic types.
  • Handling of circular object references and different ways of serialization.
  • The ability to configure custom serialization of -party classes.
  • A great alternative to similar packages like class-transformer, TypedJSON and jackson-js.

Want to know more? Let's dive into the details.

Installation

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.

Give us a star :star:

If you like or are using this project, please give it a star. Thanks!

Detailed Documentation

Understanding TypeManager

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.

Defining decorators

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

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

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

Extension points exported contracts — how you extend this code

NamingConvention (Interface)
(no doc) [19 implementers]
src/naming-convention.ts
TypeConfiguration (Interface)
(no doc) [7 implementers]
src/type-configuration.ts
InjectSorter (Interface)
(no doc) [6 implementers]
src/inject-sorter.ts
Serializer (Interface)
(no doc) [24 implementers]
src/serializer.ts
ReferenceHandler (Interface)
(no doc) [6 implementers]
src/reference-handler.ts
PropertySorter (Interface)
(no doc) [6 implementers]
src/property-sorter.ts
Injector (Interface)
(no doc) [3 implementers]
src/injector.ts
Factory (Interface)
(no doc) [2 implementers]
src/factory.ts

Core symbols most depended-on inside this repo

Property
called by 129
src/property.ts
get
called by 78
src/injector.ts
Type
called by 67
src/type.ts
deserialize
called by 64
src/serializer.ts
serialize
called by 60
src/serializer.ts
set
called by 59
src/property-metadata.ts
convert
called by 49
src/naming-convention.ts
error
called by 42
src/logger.ts

Shape

Method 422
Class 253
Function 19
Interface 11
Enum 5

Languages

TypeScript100%

Modules by API surface

src/type-metadata.ts83 symbols
src/property-metadata.ts56 symbols
src/type-states/unresolved-type-state.ts42 symbols
src/serializer-context.ts33 symbols
src/type-manager.ts31 symbols
src/property-states/unresolved-property-state.ts25 symbols
spec/use-cases/polymorphic-types.spec.ts15 symbols
spec/use-cases/complex-polymorphic-types.spec.ts13 symbols
src/inject-metadata.ts12 symbols
spec/type-extension-metadata.spec.ts12 symbols
src/metadata.ts10 symbols
spec/use-cases/reflect-metadata.spec.ts10 symbols

For agents

$ claude mcp add TypeManager.TS \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact