Type-safe, decorator-based model fields for TypeScript. Automatically coerce primitives, validate enums, materialize relations, and handle lists with a small, dependency-light API built on reflect-metadata.
- Primitives:
@Field(String|Number|Boolean)coerces assigned values - Lists:
@List(String|Number|Boolean)coerces each element - Enums:
@Enum(MyEnum)enforces membership (throws if invalid) - Relations:
@ObjectField(Model)and@ObjectList(Model)build model instances from plain objects - Auto wiring:
@AutoModel()ensures descriptors are bound on instances at construction time - Utilities:
Model.create(),Model.hasMany(),Model.belongsTo(),Model#toString()
npm i typed-fieldsThis package imports reflect-metadata as a side effect from its main entry, so you typically don’t need to import it yourself when you import from typed-fields. If you import submodules directly, add:
import 'reflect-metadata';Enable decorators and (optionally) metadata in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,
"module": "ESNext",
"target": "ES5",
"moduleResolution": "Node",
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] }
}
}Jest setup (optional) – if you use Jest, map the alias and ensure reflect-metadata loads first (already covered by the library’s main export):
// jest.config.cjs
module.exports = {
transform: {
'^.+\\.(t|j)sx?$': ['ts-jest', { useESM: true, tsconfig: '<rootDir>/tsconfig.json' }],
},
moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1' },
extensionsToTreatAsEsm: ['.ts'],
setupFiles: ['reflect-metadata']
};import { Model, Field, List, Enum, ObjectField, ObjectList, MomentField, DayjsField } from 'typed-fields';
enum Role { User = 'user', Admin = 'admin' }
class Profile extends Model {
@Field(String) bio?: string;
@MomentField() birthday?: any; // moment object
@DayjsField() createdAt?: any; // dayjs object
}
class User extends Model {
@Field(Number) id?: number;
@Field(String) name?: string;
@Enum(Role) role?: Role;
@ObjectField(Profile) profile?: Profile;
@List(String) tags?: string[];
}
const u = User.create<User>();
u.id = '42' as any; // -> 42
u.role = 'admin' as any; // ok, matches enum member
u.tags = [1, '2', true] as any; // -> ['1','2','true']
u.profile = { bio: 'Hello', birthday: '2024-01-01', createdAt: '2024-01-01' }; // -> Profile instance with moment birthday and dayjs createdAt-
@Field(prototype)- Coerces primitives using the provided constructor (
String,Number,Boolean; custom constructors allowed if they accept a single value). - Stores the raw coerced value via metadata; property becomes a getter/setter.
- Coerces primitives using the provided constructor (
-
@List(prototype)- Coerces each element of an assigned array using the provided constructor.
null/undefinedelements are preserved.
-
@Enum(enumObject)- Ensures the assigned value is one of
Object.values(enumObject); otherwise throws.
- Ensures the assigned value is one of
-
@ObjectField(Constructor?)- Accepts a plain object and materializes it into an instance (prefers static
create()if available, otherwisenew) thenObject.assigns the value. - If
Constructoris omitted, uses the declaring class constructor.
- Accepts a plain object and materializes it into an instance (prefers static
-
@ObjectList(Constructor?)- Like
@ObjectFieldbut for arrays of related objects.
- Like
-
@MomentField()- Converts assigned values to
moment(value); preservesnull/undefined.
- Converts assigned values to
-
@DayjsField()- Converts assigned values to
dayjs(value); preservesnull/undefined.
- Converts assigned values to
-
@AutoModel()(class decorator)- Ensures property descriptors collected during decoration are defined on each instance at construction time. Useful in complex initialization flows.
Model.create<T>()– returns an object created with the class prototype (Object.create(this.prototype)), handy for lightweight instances.Model.hasMany(Parent, Child, field)– apply an@ObjectList(Child)relationship toParent.prototype[field](helps with circular type references).Model.belongsTo(Child, Parent, field)– apply an@ObjectField(Parent)relationship toChild.prototype[field].Model#toString()–JSON.stringify(this)convenience.
class Author extends Model {
@List(String) names?: string[];
}
class Book extends Model {
@ObjectField(Author) author?: Author;
}
// When types depend on each other, use the helpers:
Model.hasMany(Author, Book, 'books'); // author.books: Book[]
Model.belongsTo(Book, Author, 'author'); // book.author: Author- The first time you assign to a decorated property, the library defines a non-configurable accessor on the instance that stores/reads its value from metadata.
- Assigning
null/undefinedpreserves those values. - For relation decorators, if the related constructor defines a static
create()method, it is used to instantiate; otherwise the class is constructed withnew.
// Decorators
Field(prototype: (...args: any[]) => any): PropertyDecorator
List(prototype: (...args: any[]) => any): PropertyDecorator
Enum(enumObject: Record<string, string | number | boolean>): PropertyDecorator
ObjectField(constructor?: new (...args: any[]) => any): PropertyDecorator
ObjectList(constructor?: new (...args: any[]) => any): PropertyDecorator
MomentField(): PropertyDecorator
DayjsField(): PropertyDecorator
AutoModel<T extends { new (...rest: any[]): {} }>(): ClassDecorator
// Base class
class Model {
static create<T = Model>(): T
static hasMany<T1 extends Model, T2 extends Model>(Parent: new () => T1, Child: new () => T2, field: keyof T1): void
static belongsTo<T1 extends Model, T2 extends Model>(Child: new () => T1, Parent: new () => T2, field: keyof T1): void
toString(): string
}- Build:
npm run build(outputs todist/) - Test:
npm test(Jest + ts-jest) - CI: GitHub Actions workflow at
.github/workflows/ci.ymlruns build and tests on Node 18 and 20 for pushes and PRs
MIT © thanhtunguet