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
- A1: Use type tag
- 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
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 bothuint32
anddouble
(number & Type<"int32">) | (bigint & Type<"uint64">)
number
isint32
bigint
isuint64
(number & (Type<"int32">)| Type<"float">) | (bigint & Type<"uint64">)
number
can be bothint32
andfloat
bigint
isuint64
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}>
byte
password
regex
uuid
email
hostname
idn-email
idn-hostname
iri
iri-reference
ipv4
ipv6
uri
uri-reference
uri-template
url
date-time
date
time
duration
json-pointer
relative-json-pointer
- array
Array<T> & MinItems<{number}>
Array<T> & MaxItems<{number}>
Array<T> & UniqueItems
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.
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.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>
>;
}
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}
byte
password
regex
uuid
email
hostname
idn-email
idn-hostname
iri
iri-reference
ipv4
ipv6
uri
uri-reference
uri-template
url
date-time
date
time
duration
json-pointer
relative-json-pointer
- array
@minItems {number}
@maxItems {number}
@uniqueItems
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).
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;
/**
* @Pattern /^[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
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.
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;
})()`;
}>;