๐Ÿ“– Guide Documents
Runtime Validators
Special Tags

Outline

typia can perform additional validation through type tags and comment tags.

When you need additional validation logic that is not supported in pure TypeScript type spec, you can use type tags and comment tags for it. For example, if you define a type with intersection symbol like number & typia.tags.Type<"uint32"> and validates it, typia will check the target numeric value is unsigned integer or not.

Also, in TypeScript (and JavaScript), writing @ character in comment is called Comment Tag and typia utilizes such comment tags for enhancing type validation logic. As you can see from below example code, typia analyzes @tagName value patterned comment tags, and generates optimal validation logic in the compilation level.

Therefore, don't be afraid typia uses only pure TypeScript types for type validation schema. Don't be afraid about TypeScript does not support integer type. With those type tags and comment tags, you can express every types in the world.

  • Q: How to validate integer type? TypeScript does not support it
    • A1: Use type tag number & typia.tags.Type<"int32">
    • A2: Write a comment tag @type int32 on the target property
  • Q: Type Tag vs Comment Tags, which one is better
    • A1: Type Tag is recommended because it is much safer and generous
    • A2: Comment Tag is designed for legacy JSDoc styled projects
is.tag.ts
import typia, { tags } from "typia";
 
export const checkCustomTag = typia.createIs<CustomTag>();
 
interface CustomTag {
    /**
     * @type uint32
     */
    type: number;
 
    number?: number & tags.ExclusiveMinimum<19> & tags.Maximum<100>;
 
    /**
     * @minLength 3
     */
    string: string;
 
    pattern: string & tags.Pattern<"^[a-z]+$">;
 
    /**
     * Type tag can perform union type.
     * 
     * In here case, format can be oneof `ipv4` or `ipv6` format.
     */
    format: (string & (tags.Format<"ipv4"> | tags.Format<"ipv6">)) | null;
 
    /**
     * In the Array case, only type tag can restrict element type.
     */
    array: Array<string & tags.Format<"uuid">> 
        & tags.MinItems<3> 
        & tags.MaxItems<100>;
 
    /**
     * Also, only type tag can handle map type.
     */
    map: Map<
        number & tags.Type<"uint32">, 
        Array<string & tags.Format<"uuid">> & tags.MinItems<1>
    >;
}

Type Tags

By using type tags, you can utilize additional validation logics.

Just import one of type tags from typia, and combine it with target through intersection symbol like number & typia.tags.Type<"uint32"> case. If you want to declare an union validation logic, combine | and bracket (()) symbols properly like below:

  • number & (Type<"uint32"> | Type<"double">)
    • number type can be both uint32 and double
  • (number & Type<"int32">) | (bigint & Type<"uint64">)
    • number is int32
    • bigint is uint64
  • (number & (Type<"int32">)| Type<"float">) | (bigint & Type<"uint64">)
    • number can be both int32 and float
    • bigint is uint64

Here is the entire list of type tags that typia basically supports.

For reference, when you take a mistake that choosing different target type, TypeScript compiler would block it with compilation error message. Also, if you take a mistake that placing invalid argument on the type, it would also be blocked IDE and compiler. Therefore, have a confidence when using them.

  • number
    • number & Type<{keyword}>
      • int32
      • uint32
      • uint64
      • int64
      • float
      • double
    • number & Minimum<{number}>
    • number & Maximum<{number}>
    • number & ExclusiveMaximum<{number}>
    • number & ExclusiveMinimum<{number}>
    • number & MultipleOf<{number}>
  • bigint
  • bigint & Type<{keyword}>
    • int64
    • uint64
  • bigint & Minimum<{bigint}>
  • bigint & Maximum<{bigint}>
  • bigint & ExclusiveMaximum<{bigint}>
  • bigint & ExclusiveMinimum<{bigint}>
  • bigint & MultipleOf<{bigint}>
  • string
    • string & MinLength<{number}>
    • string & MaxLength<{number}>
    • string & Pattern<{regex}>
    • string & Format<{keyword}>
      • email
      • uuid
      • ipv4
      • ipv6
      • url
      • date: YYYY-MM-DD
      • date-time: Date.toISOString()

Also, if you need custom validation logic, just make it by yourself referencing Customization section. It is easy to define. For such type safety and generous use case reasons even customization supporting, I recommend you to use type tags instead of comment tags, unless you are maintaining a legacy JSDoc styled project.

is.tag.ts
import typia, { tags } from "typia";
 
export const checkCustomTag = typia.createIs<CustomTag>();
 
interface CustomTag {
    type: number & tags.Type<"uint32">;
 
    number?: number & tags.ExclusiveMinimum<19> & tags.Maximum<100>;
 
    string: string & tags.MinLength<3>;
 
    pattern: string & tags.Format<"^[a-z]+$">;
 
    /**
     * Type tag can perform union type.
     * 
     * In here case, format can be oneof `ipv4` or `ipv6` format.
     */
    format: (string & (tags.Format<"ipv4"> | tags.Format<"ipv6">)) | null;
 
    /**
     * In the Array case, only type tag can restrict element type.
     */
    array: Array<string & tags.Format<"uuid">> 
        & tags.MinItems<3> 
        & tags.MaxItems<100>;
 
    /**
     * Also, only type tag can handle map type.
     */
    map: Map<
        number & tags.Type<"uint32">, 
        Array<string & tags.Format<"uuid">> & tags.MinItems<1>
    >;
}

Comment Tags

typia supports those comment tags, too.

Here is the entire list of comment tags that typia supports.

  • number
    • @type {string}
      • int / int32
      • uint / uint32
      • int64
      • uint64
      • float
    • @minimum {number}
    • @maximum {number}
    • @exclusiveMinimum {number}
    • @exclusiveMaximum {number}
    • @multipleOf {number}
  • bigint
    • @type uint64
    • @minimum {bigint}
    • @maximum {bigint}
    • @exclusiveMinimum {bigint}
    • @exclusiveMaximum {bigint}
    • @multipleOf {bigint}
  • string
    • @minLength {number}
    • @maxLength {number}
    • @pattern {regex}
    • @format {keyword}
      • email
      • uuid
      • ipv4
      • ipv6
      • url
      • date: YYYY-MM-DD
      • date-time: Date.toISOString()
  • array
    • @minItems {number}
    • @maxItems {number}

By the way, I do not recommend this way, because it can't perform union numeric types, and can be used for only object property type. It can't be used standalone, and cannot be used for element type of Array and Map even when they're declared on object property. Also, When you declare @type int32 statement, target number type be fixed as int32 type, and never can have another numeric type by declaring union statements.

Also, those comment tags are not type safe. If you take a mistake when writing a comment tag, it will not be detected by the compiler, and will cause an error at runtime. For example, if you write a mis-spelled keyword like @type unit32, the target number type would be double type, and you can identify it just by running the program (or visiting playground website).


is.tag.ts
import typia from "typia";
 
export const checkCustomTag = typia.createIs<CustomTag>();
 
interface CustomTag {
    /**
     * @type uint32
     */
    type: number;
 
    /**
     * @exclusiveMinimum 19
     * @maximum 100
     */
    number?: number;
 
    /**
     * @minLength 3
     */
    string: string;
 
    /**
     * @format /^[a-z]+$/
     */
    pattern: string;
 
    // NO WAY WHEN COMMENT TAG
    // /**
    //  * Type tag can perform union type.
    //  * 
    //  * In here case, format can be oneof `ipv4` or `ipv6` format.
    //  */
    // format: (string & (tags.Format<"ipv4"> | tags.Format<"ipv6">)) | null;
 
    // NO WAY WHEN COMMENT TAG
    // /**
    //  * In the Array case, only type tag can restrict element type.
    //  */
    // array: Array<string & tags.Format<"uuid">> 
    //     & tags.MinItems<3> 
    //     & tags.MaxItems<100>;
 
    // NO WAY WHEN COMMENT TAG
    // /**
    //  * Also, only type tag can handle map type.
    //  */
    // map: Map<
    //     number & tags.Type<"uint32">, 
    //     Array<string & tags.Format<"uuid">> & tags.MinItems<1>
    // >;
}

Customization

TagBase.ts
export type TagBase<Props extends TagBase.IProps<any, any, any, any, any>> = {
    /**
     * This is a dummy property for compilation.
     * 
     * It does not mean anything in runtime.
     */
    "typia.tag"?: Props;
};
export namespace TagBase {
    export interface IProps<
        Target extends "bigint" | "number" | "string" | "array",
        Kind extends string,
        Value extends boolean | bigint | number | string | undefined,
        Validate extends
            | string
            | {
                  [key in Target]?: string;
              },
        Exclusive extends boolean | string[],
    > {
        /**
         * Target type.
         *
         * If user tries to adapt this tag to a different type, it would be a compile
         * error.
         *
         * For example, you've configured target type as `string`, but user adapted it
         * onto a `number` type (`number & YourCustomTag<Value>`), then it would be
         * blocked by TypeScript compiler.
         */
        target: Target;
 
        /**
         * What kind of tag is this?
         */
        kind: Kind;
 
        /**
         * Value to be configured by user.
         */
        value: Value;
 
        /**
         * Validation code.
         *
         * This code would be inserted into the generated validation function.
         * In here script, target variable name must be `$input`. The variable name
         * `$input` would be transformed to the suitable when compilation.
         *
         * Also, If you've take a mistake on this script, compile error would be
         * occured. So, define it with confidence. Compiler will block all your
         * mistakes.
         */
        validate: Validate;
 
        /**
         * Exclusive option.
         *
         * If this property configured as `true`, same {@link kind} tag cannot be
         * duplicated in the target type. Otherwise, if you've configured this property
         * as string array, all of the {@link kind} value assigned tag cannot be
         * compatible in the target type.
         * 
         * @default false
         */
        exclusive?: Exclusive | string[];
    }
}

Above types are supported by typia basically.

If you make a custom type tag extending typia.tags.TagBase<Props> type, and utilize it on your type with intersection symbol like number & Minimum<3>, its validation logic 3 <= $input would be inserted into the compiled JavaScript file.

Also, as you can see from the typia.tags.TagBase<Props> type, you have to specify which target type is the tag for, and need to define the tag can be compatible with others or not through exclusive options. If your custom tag has multiple target types, you can support all of those target types by defining validate property as Record<Target, string> type like Type tag case.

In the Korean proverb, there's a word that, "it is much better to do it once than to hear it a hundred times". Let's see how custom type tag of typia can be defined and utilized through an example code. I'll define three custom tag types, Postfix, Dollar and IsEven.

Here is the example code, and I think that it may easy to understand.

is.tag.custom.ts
import typia from "typia";
 
export const checkTagCustom = typia.createIs<TagCustom>();
 
interface TagCustom {
    id: string & typia.tags.Format<"uuid">;
    dollar: string & Dolloar;
    postfix: string & Postfix<"abcd">;
    powerOf: number & PowerOf<2>;
}
 
type Dolloar = typia.tags.TagBase<{
    kind: "dollar";
    target: "string";
    value: undefined;
    validate: `$input[0] === "$" && !isNaN(Number($input.substring(1).split(",").join("")))`;
}>;
 
type Postfix<Value extends string> = typia.tags.TagBase<{
    kind: "postfix";
    target: "string";
    value: Value;
    validate: `$input.endsWith("${Value}")`;
}>;
 
type PowerOf<Value extends number> = typia.tags.TagBase<{
    kind: "powerOf";
    target: "number";
    value: Value;
    validate: `(() => {
        const denominator: number = Math.log(${Value});
        const value: number = Math.log($input) / denominator;
        return Math.abs(value - Math.round(value)) < 0.00000001;
    })()`;
}>;