bookish-potato-dto
Version:
## Overview A TypeScript decorators-based API for defining Data Transfer Object (DTO) classes, types, and parsers. Simplifies schema validation and type enforcement using intuitive decorators and TypeScript classes.
669 lines (536 loc) • 23.8 kB
Markdown
# TypeScript Decorators API for DTOs
## Overview
A TypeScript decorators-based API for defining Data Transfer Object (DTO) classes, types, and parsers. Simplifies schema validation and type enforcement using intuitive decorators and TypeScript classes.
## Table of Contents
- [Feature Requests, Bugs Reports, and Contributions](#feature-requests-bugs-reports-and-contributions)
- [Usage Guide](#usage-guide)
- [Defining DTOs](#defining-dtos)
- [How to define DTO classes with decorators.](#how-to-define-dto-classes-with-decorators)
- [Supported types and examples.](#supported-types-and-examples)
- [String](#string)
- [Regular Expressions](#regular-expressions)
- [Number](#number)
- [Integer](#integer)
- [Boolean](#boolean)
- [Enum](#enum)
- [Date](#date)
- [Arrays of primitives](#arrays-of-primitives)
- [Arrays of DTOs](#arrays-of-dtos)
- [Nested DTOs](#nested-dtos)
- [Custom](#custom)
- [Extending DTOs](#extending-dtos)
- [Use `extends` keyword to extend DTOs.](#use-extends-keyword-to-extend-dtos)
- [Decorating the DTO class (deprecated)](#decorating-the-dto-class-deprecated)
- [Possible Use Cases](#possible-use-cases)
- [API requests and responses](#api-requests-and-responses)
- [Configuration files](#configuration-files)
- [Data transformation](#data-transformation)
- [Configuration](#configuration)
- [Decorators Options](#decorators-options)
- [Common configurable options](#common-configurable-options)
- [Strict data types](#strict-data-types)
- [`@StringProperty`](#stringproperty)
- [`@RegexProperty`](#regexproperty)
- [`@NumberProperty`](#numberproperty)
- [`@IntegerProperty`](#integerproperty)
- [`@BooleanProperty`](#booleanproperty)
- [`@EnumProperty`](#enumproperty)
- [`@DateProperty`](#dateproperty)
- [`@ArrayProperty`](#arrayproperty)
- [`@ArrayDtoProperty`](#arraydtoproperty)
- [`@DtoProperty`](#dtoproperty)
- [`@CustomProperty`](#customproperty)
- [`@DtoClass`](#dtoclass)
- [Parsing and Validation](#parsing-and-validation)
- [Using custom parsers.](#using-custom-parsers)
- [Advanced Features](#advanced-features)
- [Custom decorators for additional functionality.](#custom-decorators-for-additional-functionality)
## Feature Requests, Bugs Reports, and Contributions
Please use the [GitHub Issues](https://github.com/andrei-trukhin/bookish-potato-dto-issues)
repository to report bugs, request features, or ask questions.
## Usage Guide
### Defining DTOs
#### How to define DTO classes with decorators.
Decorators allow you to define properties with validation and transformation rules:
```typescript
class PersonDTO {
@StringProperty()
readonly name!: string;
@IntegerProperty({
strictDataTypes: true,
})
readonly age!: number;
@NumberProperty()
readonly height!: number;
@NumberProperty({
defaultValue: 70
})
readonly weight!: number;
@StringProperty({
isOptional: true
})
readonly eyeColor?: string;
@BooleanProperty()
readonly active!: boolean;
}
const person = parseObject(PersonDTO, {
name: "John Doe",
age: 30,
height: "180.5",
active: true,
email: "email@mail.com" // won't be assigned to the object as it is not a property of PersonDTO
});
// person is now an instance of PersonDTO with the values set.
// person.name === "John Doe"
// person.age === 30
// person.height === 180
// person.active === true
// person.weight === 70
// person.eyeColor === undefined
```
#### Supported types and examples.
##### String
```typescript
class ExampleDTO {
@StringProperty()
readonly name!: string;
}
```
The `StringProperty` decorator validates and assigns a required string value to the property, ensuring it is of type `string`.
##### Regular Expressions
```typescript
class ExampleDTO {
@RegexProperty(/^[a-zA-Z0-9]{3,10}$/)
readonly name!: string;
}
```
The `RegexProperty` decorator validates the string value against a regular expression.
##### Number
```typescript
class ExampleDTO {
@NumberProperty()
readonly age!: number;
}
```
The `NumberProperty` decorator validates and assigns a required number value to the property, ensuring it is of type number.
##### Integer
```typescript
class ExampleDTO {
@IntegerProperty()
readonly height!: number;
}
```
The `IntegerProperty` decorator validates and assigns a required integer value. Strings are converted to integers if possible.
##### Boolean
```typescript
class ExampleDTO {
@BooleanProperty()
readonly active!: boolean;
}
```
The `BooleanProperty` decorator validates and assigns a required boolean value to the property.
Strings such as "true" and "false" will be converted to boolean values by default.
##### Enum
```typescript
enum Colors {
Red = 'red',
Green = 'green',
Blue = 'blue'
}
enum ColorType {
Primary = 1,
Secondary = 2
}
class ButtonDTO {
@EnumProperty(Colors)
readonly color!: Colors;
@EnumProperty(ColorType)
readonly colorType!: ColorType;
}
const example = parseObject(ExampleDTO, {
color: 'red',
colorType: 1
});
// example.color === Colors.Red
// example.colorType === ColorType.Primary
```
The `EnumProperty` decorator validates and assigns a required enum value to the property.
##### Date
```typescript
class ExampleDTO {
@DateProperty()
readonly date!: Date;
@DateProperty({
defaultValue: new Date('2022-01-01')
})
readonly defaultDate!: Date;
@DateProperty({
defaultValue: "January 3, 2025"
})
readonly stringDate!: Date;
}
```
The `DateProperty` decorator validates and assigns a required date value to the property.
##### Arrays of primitives
```typescript
class ExampleDTO {
@ArrayProperty('string')
readonly names!: string[];
@ArrayProperty('number')
readonly ages!: number[];
@ArrayProperty('boolean')
readonly active!: boolean[];
}
```
The `ArrayProperty` decorator validates arrays of primitive types such as `string`, `number`, or `boolean`.
##### Arrays of DTOs
```typescript
class AddressDTO {
@StringProperty()
readonly street!: string;
}
class PersonDTO {
@ArrayDtoProperty(AddressDTO)
readonly addresses!: AddressDTO[];
}
```
The `ArrayDtoProperty` decorator is used for arrays of DTOs.
##### Nested DTOs
```typescript
class AddressDTO {
@StringProperty()
readonly street!: string;
}
class PersonDTO {
@DtoProperty(AddressDTO)
readonly address!: AddressDTO;
}
```
The `DtoProperty` decorator is used for nested DTOs.
##### Custom
```typescript
class CustomParser implements PropertyParser<boolean> {
parse(value: unknown): boolean {
if (value === "1") return true;
if (value === "0") return false;
throw new Error("Invalid value: " + value);
}
}
class PersonDTO {
@CustomProperty({ parser: new CustomParser() })
readonly active!: boolean;
}
const example = parseObject(PersonDTO, {
active: '1'
});
// example.active === true
```
### Extending DTOs
#### Use `extends` keyword to extend DTOs.
```typescript
class PersonDTO {
@StringProperty()
readonly name!: string;
@IntegerProperty()
readonly age!: number;
}
class EmployeeDTO extends PersonDTO {
@StringProperty()
readonly position!: string;
}
```
#### Decorating the DTO class (deprecated)
<span style="color:orange">(!) The `DtoClass` decorator is deprecated. Use the `extends` keyword instead.</span>.
You can extend DTOs to reuse properties and add new ones.
The following example demonstrates how to extend a DTO:
```typescript
class PersonDTO {
@StringProperty()
readonly name!: string;
@IntegerProperty()
readonly age!: number;
}
@DtoClass({
extends: [PersonDTO]
})
class EmployeeDTO {
@StringProperty()
readonly position!: string;
}
```
The `DtoClass` decorator extends a DTO class using the `extends` option to specify the parent class(es).
Multiple parents can be defined as well.
**Note**: Parent and child DTO classes cannot have overlapping property names. Defining properties with the same name in both will cause an error.
### Possible Use Cases
- **API requests and responses**: Validate and parse API requests and responses.
- **Configuration files**: Validate and parse configuration files.
- **Data transformation**: Transform data from one format to another.
#### API requests and responses
The following example demonstrates how to use DTOs to validate and parse API requests and responses:
```typescript
enum Roles {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest',
}
class RoleDTO {
@StringProperty()
readonly name!: string;
@StringProperty()
readonly description!: string;
@EnumProperty(Roles)
readonly role!: Roles;
}
class UserDTO {
@StringProperty()
readonly name!: string;
@StringProperty()
readonly email!: string;
@IntegerProperty()
readonly age!: number;
@DtoProperty(RoleDTO)
readonly role!: RoleDTO;
}
const req = fetch('https://api.example.com/user/1')
.then(response => response.json())
.then(data => {
const user = parseObject(UserDTO, data);
console.log('User:', user);
});
```
#### Configuration files
The following example demonstrates how to use DTOs to validate and parse configuration files.
Imagine you have the following environment variables:
```shell
PORT=8080
LOG_LEVEL=debug
MAX_CONNECTIONS=20
DATA_BASE_SECRET=secret
WHITE_LISTED_URLS=example.com,example.org
```
You can use the following DTO to parse the environment variables:
```typescript
enum LogLevel {
INFO = 'info',
DEBUG = 'debug',
WARN = 'warn',
ERROR = 'error',
}
class ArrayParser implements PropertyParser<string[]> {
constructor(private readonly separator: string = ',') {
}
parse(value: unknown): string[] {
if (typeof value !== 'string') {
throw new ParsingError(`Value is not a string! Cannot parse to array.`);
}
const array = value.split(this.separator);
if (array.length === 1 && array[0] === '') {
return [];
}
return array;
}
}
/**
* The configuration for the application.
*/
export class EnvironmentConfig {
/**
* The port the application should listen on.
*/
@IntegerProperty({
mapFrom: 'PORT',
defaultValue: 3000
})
readonly port!: number;
/**
* The log level for the application.
*/
@EnumProperty(LogLevel, {
mapFrom: 'LOG_LEVEL',
defaultValue: LogLevel.INFO,
})
readonly logLevel!: LogLevel;
/**
* The maximum number of connections to the data source.
*/
@IntegerProperty({
mapFrom: 'MAX_CONNECTIONS',
defaultValue: 10,
})
readonly maxConnections!: number;
/**
* The base URL for the data source.
*/
@StringProperty({
mapFrom: 'DATA_BASE_URL',
defaultValue: 'localhost:5432',
})
readonly dataBaseUrl!: string;
@StringProperty({
mapFrom: 'DATA_BASE_SECRET',
})
readonly dataBaseSecret!: string;
@CustomProperty({
mapFrom: 'WHITE_LISTED_URLS',
parser: new ArrayParser(),
})
readonly whiteListedUrls!: string[];
}
// Create an instance of the configuration.
console.log('Parse the environment variables...');
export const environmentConfig =
parseObject(EnvironmentConfig, process.env);
console.log('Environment configuration:', environmentConfig);
// Output the configuration.
// environmentConfig.port === 8080
// environmentConfig.logLevel === LogLevel.DEBUG
// environmentConfig.maxConnections === 20
// environmentConfig.dataBaseUrl === 'localhost:5432'
// environmentConfig.dataBaseSecret === 'secret'
// environmentConfig.whiteListedUrls === ['example.com', 'example.org']
```
The advantage of using DTOs is that you can define the configuration schema in one place and reuse it throughout the application.
See also the [MiddlewareConfiguration](https://github.com/andrei-trukhin/bookish-potato-dto-issues/blob/main/nextjs-example-bookish-potato-dto/src/app/_config/middleware.configuration.ts) class in the [Next.js example](https://github.com/andrei-trukhin/bookish-potato-dto-issues/tree/main/nextjs-example-bookish-potato-dto)
#### Data transformation
The following example demonstrates how to use DTOs to transform data from one format to another:
```typescript
interface User {
readonly uuid: string;
readonly name: string;
readonly age: number;
readonly lastLogin: Date;
}
class Person {
@StringProperty({
mapFrom: 'uuid',
})
readonly id!: string;
@StringProperty()
readonly name!: string;
@StringProperty({
defaultValue: 'active',
})
readonly status!: string;
@CustomProperty({
defaultValue: 1,
useDefaultValueOnParseError: true,
mapFrom: 'lastLogin',
parser: {
parse(value: unknown): number {
if (value instanceof Date) {
return new Date(Date.now()).getDay() - value.getDay();
}
throw new ParsingError('Value is not a date!');
}
},
})
readonly lastSeenOnlineDaysAgo!: number;
}
const user: User = {
uuid: '123',
name: 'John Doe',
age: 30,
lastLogin: new Date('2024-01-01'),
}
export const person = parseObject(Person, user);
console.log('Person:', person);
// Output the person object.
// person.id === '123'
// person.name === 'John Doe'
// person.status === 'active'
// person.lastSeenOnlineDaysAgo === 2 (some value based on the current date and the last login date)
```
### Configuration
#### Decorators Options
##### Common configurable options
| **Option** | **Type** | **Description** | **Default Behavior** |
|----------------|---------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| `isOptional` | `boolean` | Indicates if the property is optional. If `true`, the property is not required. | By default, all properties are required unless specified as optional. |
| `defaultValue` | `number \| string \| boolean \| (any valid type)` | Specifies a default value for the property if it's not provided. The data type should match the expected type. | Default value is empty by default. |
| `useDefaultValueOnParseError` | `boolean` | If `true`, the default value is used when parsing fails. If `false`, an error is thrown. | By default, an error is thrown when parsing fails. |
| `mapFrom` | `string` | Specifies the key in the input object to map the property from. | By default, the property name is used as the key in the input object. |
##### Strict data types
| **Option** | **Type** | **Description** | **Default Behavior** |
|-------------------|-----------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| `strictDataTypes` | `boolean` | Enforces strict type matching without conversions. When `true`, values must match the expected type exactly. | By default, the library will attempt to convert values to the expected type. |
##### `@StringProperty`
Includes [common options](#common-configurable-options) and the following:
| **Option** | **Type** | **Description** | **Default Behavior** |
|------------|-----------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| `minLength` | `number` | Specifies the minimum length of the string. | By default, there is no minimum length requirement. |
| `maxLength` | `number` | Specifies the maximum length of the string. | By default, there is no maximum length requirement. |
##### `@RegexProperty`
Includes [common options](#common-configurable-options)
##### `@NumberProperty`
Includes [common options](#common-configurable-options), [strict data types](#strict-data-types), and the following:
| **Option** | **Type** | **Description** | **Default Behavior** |
|------------|-----------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| `minValue` | `number` | Specifies the minimum value of the number. | By default, there is no minimum value requirement. |
| `maxValue` | `number` | Specifies the maximum value of the number. | By default, there is no maximum value requirement. |
##### `@IntegerProperty`
Includes [common options](#common-configurable-options), [strict data types](#strict-data-types), and the following:
| **Option** | **Type** | **Description** | **Default Behavior** |
|------------|-----------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| `minValue` | `number` | Specifies the minimum value of the integer. | By default, there is no minimum value requirement. |
| `maxValue` | `number` | Specifies the maximum value of the integer. | By default, there is no maximum value requirement. |
##### `@BooleanProperty`
Includes [common options](#common-configurable-options), [strict data types](#strict-data-types)
##### `@EnumProperty`
Includes [common options](#common-configurable-options)
##### `@DateProperty`
Includes [common options](#common-configurable-options)
##### `@ArrayProperty`
Includes [common options](#common-configurable-options), [strict data types](#strict-data-types), and the following:
| **Option** | **Type** | **Description** | **Default Behavior** |
|------------|-----------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| `minLength` | `number` | Specifies the minimum length of the array. | By default, there is no minimum length requirement. |
| `maxLength` | `number` | Specifies the maximum length of the array. | By default, there is no maximum length requirement. |
| `stringsLength` | `object` | Specifies the minimum and maximum length of the strings in the array. | By default, there is no minimum or maximum length requirement. |
| `numbersRange` | `object` | Specifies the minimum and maximum value of the numbers in the array. | By default, there is no minimum or maximum value requirement. |
##### `@ArrayDtoProperty`
Includes [common options](#common-configurable-options) and the following:
| **Option** | **Type** | **Description** | **Default Behavior** |
|------------|-----------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| `minLength` | `number` | Specifies the minimum length of the array. | By default, there is no minimum length requirement. |
| `maxLength` | `number` | Specifies the maximum length of the array. | By default, there is no maximum length requirement. |
##### `@DtoProperty`
Includes [common options](#common-configurable-options)
##### `@CustomProperty`
Includes [common options](#common-configurable-options)
##### `@DtoClass` - (!) __deprecated__
| **Option** | **Type** | **Description** | **Default Behavior** |
|------------|-----------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|
| `extends` | `Class[]` | Specifies the parent classes to extend. | By default, the class does not extend any other classes. |
### Parsing and Validation
#### Use strict data types
Enable `strictDataTypes` to enforce exact type matching without conversions:
```typescript
class PersonDTO {
@IntegerProperty({
strictDataTypes: true
})
readonly age!: number;
}
const _person = parseObject(PersonDTO, {
age: '30' // throws an error since the value is a string
});
const person = parseObject(PersonDTO, {
age: 30 // works fine
});
```
#### Using custom parsers.
You can use the `CustomProperty` decorator for advanced parsing:
```typescript
class PersonDTO {
@CustomProperty({
parser: new CustomParser()
})
readonly active!: boolean;
}
```
### Advanced Features
#### Custom decorators for additional functionality.
You can create custom decorators to add additional functionality to DTO properties.
See the [StringToArrayOfStrings](https://github.com/andrei-trukhin/bookish-potato-dto-issues/blob/main/nextjs-example-bookish-potato-dto/src/app/_custom-decorators/string-to-array.ts) example in the [Next.js example](https://github.com/andrei-trukhin/bookish-potato-dto-issues/tree/main/nextjs-example-bookish-potato-dto)
### Back to Top
[Table of Contents](#table-of-contents)