# 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)