1 | import { Runtype, RuntypeBase, Static } from '../runtype';
|
2 | import { LiteralBase } from './literal';
|
3 | declare type TemplateLiteralType<A extends readonly LiteralBase[], B extends readonly RuntypeBase<LiteralBase>[]> = A extends readonly [infer carA, ...infer cdrA] ? carA extends LiteralBase ? B extends readonly [infer carB, ...infer cdrB] ? carB extends RuntypeBase<LiteralBase> ? cdrA extends readonly LiteralBase[] ? cdrB extends readonly RuntypeBase<LiteralBase>[] ? `${carA}${Static<carB>}${TemplateLiteralType<cdrA, cdrB>}` : `${carA}${Static<carB>}` : `${carA}${Static<carB>}` : `${carA}` : `${carA}` : '' : '';
|
4 | export interface Template<A extends readonly [string, ...string[]], B extends readonly RuntypeBase<LiteralBase>[]> extends Runtype<A extends TemplateStringsArray ? string : TemplateLiteralType<A, B>> {
|
5 | tag: 'template';
|
6 | strings: A;
|
7 | runtypes: B;
|
8 | }
|
9 | declare type ExtractStrings<A extends readonly (LiteralBase | RuntypeBase<LiteralBase>)[], prefix extends string = ''> = A extends readonly [infer carA, ...infer cdrA] ? cdrA extends readonly any[] ? carA extends RuntypeBase<LiteralBase> ? [prefix, ...ExtractStrings<cdrA>] : carA extends LiteralBase ? [...ExtractStrings<cdrA, `${prefix}${carA}`>] : never : never : [prefix];
|
10 | declare type ExtractRuntypes<A extends readonly (LiteralBase | RuntypeBase<LiteralBase>)[]> = A extends readonly [infer carA, ...infer cdrA] ? cdrA extends readonly any[] ? carA extends RuntypeBase<LiteralBase> ? [carA, ...ExtractRuntypes<cdrA>] : carA extends LiteralBase ? [...ExtractRuntypes<cdrA>] : never : never : [];
|
11 | /**
|
12 | * Validates that a value is a string that conforms to the template.
|
13 | *
|
14 | * You can use the familiar syntax to create a `Template` runtype:
|
15 | *
|
16 | * ```ts
|
17 | * const T = Template`foo${Literal('bar')}baz`;
|
18 | * ```
|
19 | *
|
20 | * But then the type inference won't work:
|
21 | *
|
22 | * ```ts
|
23 | * type T = Static<typeof T>; // inferred as string
|
24 | * ```
|
25 | *
|
26 | * Because TS doesn't provide the exact string literal type information (`["foo", "baz"]` in this case) to the underlying function. See the issue [microsoft/TypeScript#33304](https://github.com/microsoft/TypeScript/issues/33304), especially this comment [microsoft/TypeScript#33304 (comment)](https://github.com/microsoft/TypeScript/issues/33304#issuecomment-697977783) we hope to be implemented.
|
27 | *
|
28 | * If you want the type inference rather than the tagged syntax, you have to manually write a function call:
|
29 | *
|
30 | * ```ts
|
31 | * const T = Template(['foo', 'baz'] as const, Literal('bar'));
|
32 | * type T = Static<typeof T>; // inferred as "foobarbaz"
|
33 | * ```
|
34 | *
|
35 | * As a convenient solution for this, it also supports another style of passing arguments:
|
36 | *
|
37 | * ```ts
|
38 | * const T = Template('foo', Literal('bar'), 'baz');
|
39 | * type T = Static<typeof T>; // inferred as "foobarbaz"
|
40 | * ```
|
41 | *
|
42 | * You can pass various things to the `Template` constructor, as long as they are assignable to `string | number | bigint | boolean | null | undefined` and the corresponding `Runtype`s:
|
43 | *
|
44 | * ```ts
|
45 | * // Equivalent runtypes
|
46 | * Template(Literal('42'));
|
47 | * Template(42);
|
48 | * Template(Template('42'));
|
49 | * Template(4, '2');
|
50 | * Template(Literal(4), '2');
|
51 | * Template(String.withConstraint(s => s === '42'));
|
52 | * Template(
|
53 | * Intersect(
|
54 | * Number.withConstraint(n => n === 42),
|
55 | * String.withConstraint(s => s.length === 2),
|
56 | * // `Number`s in `Template` accept alternative representations like `"0x2A"`,
|
57 | * // thus we have to constraint the length of string, to accept only `"42"`
|
58 | * ),
|
59 | * );
|
60 | * ```
|
61 | *
|
62 | * Trivial items such as bare literals, `Literal`s, and single-element `Union`s and `Intersect`s are all coerced into strings at the creation time of the runtype. Additionally, `Union`s of such runtypes are converted into `RegExp` patterns like `(?:foo|bar|...)`, so we can assume `Union` of `Literal`s is a fully supported runtype in `Template`.
|
63 | *
|
64 | * ### Caveats
|
65 | *
|
66 | * A `Template` internally constructs a `RegExp` to parse strings. This can lead to a problem if it contains multiple non-literal runtypes:
|
67 | *
|
68 | * ```ts
|
69 | * const UpperCaseString = Constraint(String, s => s === s.toUpperCase(), {
|
70 | * name: 'UpperCaseString',
|
71 | * });
|
72 | * const LowerCaseString = Constraint(String, s => s === s.toLowerCase(), {
|
73 | * name: 'LowerCaseString',
|
74 | * });
|
75 | * Template(UpperCaseString, LowerCaseString);
|
76 | * ```
|
77 | *
|
78 | * The only thing we can do for parsing such strings correctly is brute-forcing every single possible combination until it fulfills all the constraints, which must be hardly done. Actually `Template` treats `String` runtypes as the simplest `RegExp` pattern `.*` and the “greedy” strategy is always used, that is, the above runtype won't work expectedly because the entire pattern is just `^(.*)(.*)$` and the first `.*` always wins. You have to avoid using `Constraint` this way, and instead manually parse it using a single `Constraint` which covers the entire string.
|
79 | */
|
80 | export declare function Template<A extends TemplateStringsArray, B extends readonly RuntypeBase<LiteralBase>[]>(strings: A, ...runtypes: B): Template<A & [string, ...string[]], B>;
|
81 | export declare function Template<A extends readonly [string, ...string[]], B extends readonly RuntypeBase<LiteralBase>[]>(strings: A, ...runtypes: B): Template<A, B>;
|
82 | export declare function Template<A extends readonly (LiteralBase | RuntypeBase<LiteralBase>)[]>(...args: A): Template<ExtractStrings<A>, ExtractRuntypes<A>>;
|
83 | export {};
|