UNPKG

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