Skip to content

Commit 72ffd1a

Browse files
authored
Intrinsic String Mapping (#516)
1 parent 61efbc1 commit 72ffd1a

16 files changed

+426
-41
lines changed

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sinclair/typebox",
3-
"version": "0.30.0",
3+
"version": "0.30.1",
44
"description": "JSONSchema Type Builder with Static Type Resolution for TypeScript",
55
"keywords": [
66
"typescript",

readme.md

+58-5
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ License MIT
8484
- [Recursive](#types-recursive)
8585
- [Conditional](#types-conditional)
8686
- [Template Literal](#types-template-literal)
87+
- [Intrinsic String](#types-intrinsic-string)
8788
- [Indexed](#types-indexed)
8889
- [Negated](#types-negated)
8990
- [Rest](#types-rest)
@@ -486,6 +487,30 @@ The following table lists the Standard TypeBox types. These types are fully comp
486487
│ ]) │ │ } │
487488
│ │ │ │
488489
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
490+
const T = Type.Uncapitalize( │ type T = Uncapitalize<const T = { │
491+
│ Type.Literal('Hello') │ 'Hello' │ type: 'string', │
492+
│ ) │ > │ const: 'hello'
493+
│ │ │ } │
494+
│ │ │ │
495+
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
496+
const T = Type.Capitalize( │ type T = Capitalize<const T = { │
497+
│ Type.Literal('hello') │ 'hello' │ type: 'string', │
498+
│ ) │ > │ const: 'Hello'
499+
│ │ │ } │
500+
│ │ │ │
501+
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
502+
const T = Type.Uppercase( │ type T = Uppercase<const T = { │
503+
│ Type.Literal('hello') │ 'hello' │ type: 'string', │
504+
│ ) │ > │ const: 'HELLO'
505+
│ │ │ } │
506+
│ │ │ │
507+
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
508+
const T = Type.Lowercase( │ type T = Lowercase<const T = { │
509+
│ Type.Literal('HELLO') │ 'HELLO' │ type: 'string', │
510+
│ ) │ > │ const: 'hello'
511+
│ │ │ } │
512+
│ │ │ │
513+
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
489514
const T = Type.Object({ │ type T = { │ const R = { │
490515
│ x: Type.Number(), │ x: number, │ $ref: 'T'
491516
y: Type.Number() │ y: number │ } │
@@ -851,6 +876,34 @@ const R = Type.Record(T, Type.String()) // const R = {
851876
// }
852877
```
853878
879+
<a name='types-intrinsic-string'></a>
880+
881+
### Intrinsic String Types
882+
883+
TypeBox supports a set of intrinsic string mapping functions which can be used on string literals. These functions match the TypeScript string intrinsic types `Uppercase`, `Lowercase`, `Capitalize` and `Uncapitalize`. These functions are supported for literal strings, template literals and union types. The following shows the literal string usage.
884+
885+
```typescript
886+
// TypeScript
887+
888+
type A = Uncapitalize<'HELLO'> // type A = 'hELLO'
889+
890+
type B = Capitalize<'hello'> // type B = 'Hello'
891+
892+
type C = Uppercase<'hello'> // type C = 'HELLO'
893+
894+
type D = Lowercase<'HELLO'> // type D = 'hello'
895+
896+
// TypeBox
897+
898+
const A = Type.Uncapitalize(Type.Literal('HELLO')) // const A: TLiteral<'hELLO'>
899+
900+
const B = Type.Capitalize(Type.Literal('hello')) // const B: TLiteral<'Hello'>
901+
902+
const C = Type.Uppercase(Type.Literal('hello')) // const C: TLiteral<'HELLO'>
903+
904+
const D = Type.Lowercase(Type.Literal('HELLO')) // const D: TLiteral<'hello'>
905+
```
906+
854907
<a name='types-indexed'></a>
855908
856909
### Indexed Access Types
@@ -1586,11 +1639,11 @@ The following table lists esbuild compiled and minified sizes for each TypeBox m
15861639
┌──────────────────────┬────────────┬────────────┬─────────────┐
15871640
│ (index) │ CompiledMinifiedCompression
15881641
├──────────────────────┼────────────┼────────────┼─────────────┤
1589-
typebox/compiler'129.4 kb'' 58.6 kb''2.21 x'
1590-
typebox/errors'111.6 kb'' 50.1 kb''2.23 x'
1591-
typebox/system' 76.5 kb'' 31.7 kb''2.41 x'
1592-
typebox/value'180.7 kb'' 79.3 kb''2.28 x'
1593-
typebox' 75.4 kb'' 31.3 kb''2.41 x'
1642+
typebox/compiler'131.4 kb'' 59.4 kb''2.21 x'
1643+
typebox/errors'113.6 kb'' 50.9 kb''2.23 x'
1644+
typebox/system' 78.5 kb'' 32.5 kb''2.42 x'
1645+
typebox/value'182.8 kb'' 80.0 kb''2.28 x'
1646+
typebox' 77.4 kb'' 32.0 kb''2.42 x'
15941647
└──────────────────────┴────────────┴────────────┴─────────────┘
15951648
```
15961649

src/typebox.ts

+104-17
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export interface TAsyncIterator<T extends TSchema = TSchema> extends TSchema {
248248
items: T
249249
}
250250
// -------------------------------------------------------------------------------
251-
// Awaited
251+
// TAwaited
252252
// -------------------------------------------------------------------------------
253253
// prettier-ignore
254254
export type TAwaitedRest<T extends TSchema[]> = T extends [infer L, ...infer R]
@@ -394,7 +394,6 @@ export interface TFunction<T extends TSchema[] = TSchema[], U extends TSchema =
394394
// --------------------------------------------------------------------------
395395
// TIndex
396396
// --------------------------------------------------------------------------
397-
398397
export type TIndexRest<T extends TSchema[], K extends TPropertyKey> = T extends [infer L, ...infer R] ? [TIndexType<AssertType<L>, K>, ...TIndexRest<AssertRest<R>, K>] : []
399398
export type TIndexProperty<T extends TProperties, K extends TPropertyKey> = K extends keyof T ? [T[K]] : []
400399
export type TIndexTuple<T extends TSchema[], K extends TPropertyKey> = K extends keyof T ? [T[K]] : []
@@ -419,6 +418,34 @@ export type TIndex<T extends TSchema, K extends TPropertyKey[]> =
419418
T extends TTuple ? UnionType<Flat<TIndexRestMany<T, K>>> :
420419
TNever
421420
// --------------------------------------------------------------------------
421+
// TIntrinsic
422+
// --------------------------------------------------------------------------
423+
export type TIntrinsicMode = 'Uppercase' | 'Lowercase' | 'Capitalize' | 'Uncapitalize'
424+
// prettier-ignore
425+
export type TIntrinsicTemplateLiteral<T extends TTemplateLiteralKind[], M extends TIntrinsicMode> =
426+
M extends ('Lowercase' | 'Uppercase') ? T extends [infer L, ...infer R] ? [TIntrinsic<AssertType<L>, M>, ...TIntrinsicTemplateLiteral<AssertRest<R>, M>] : T :
427+
M extends ('Capitalize' | 'Uncapitalize') ? T extends [infer L, ...infer R] ? [TIntrinsic<AssertType<L>, M>, ...R] : T :
428+
T
429+
// prettier-ignore
430+
export type TIntrinsicLiteral<T, M extends TIntrinsicMode> =
431+
T extends string ?
432+
M extends 'Uncapitalize' ? Uncapitalize<T> :
433+
M extends 'Capitalize' ? Capitalize<T> :
434+
M extends 'Uppercase' ? Uppercase<T> :
435+
M extends 'Lowercase' ? Lowercase<T> :
436+
string
437+
: ''
438+
// prettier-ignore
439+
export type TIntrinsicRest<T extends TSchema[], M extends TIntrinsicMode> = T extends [infer L, ...infer R]
440+
? [TIntrinsic<AssertType<L>, M>, ...TIntrinsicRest<AssertRest<R>, M>]
441+
: []
442+
// prettier-ignore
443+
export type TIntrinsic<T extends TSchema, M extends TIntrinsicMode> =
444+
T extends TTemplateLiteral<infer S> ? TTemplateLiteral<TIntrinsicTemplateLiteral<S, M>> :
445+
T extends TUnion<infer S> ? TUnion<TIntrinsicRest<S, M>> :
446+
T extends TLiteral<infer S> ? TLiteral<TIntrinsicLiteral<S, M>> :
447+
T
448+
// --------------------------------------------------------------------------
422449
// TInteger
423450
// --------------------------------------------------------------------------
424451
export interface TInteger extends TSchema, NumericOptions<number> {
@@ -1306,8 +1333,15 @@ export namespace TypeGuard {
13061333
}
13071334
/** Returns true if the given schema is TString */
13081335
export function TString(schema: unknown): schema is TString {
1336+
// prettier-ignore
13091337
return (
1310-
TKindOf(schema, 'String') && schema.type === 'string' && IsOptionalString(schema.$id) && IsOptionalNumber(schema.minLength) && IsOptionalNumber(schema.maxLength) && IsOptionalPattern(schema.pattern) && IsOptionalFormat(schema.format)
1338+
TKindOf(schema, 'String') &&
1339+
schema.type === 'string' &&
1340+
IsOptionalString(schema.$id) &&
1341+
IsOptionalNumber(schema.minLength) &&
1342+
IsOptionalNumber(schema.maxLength) &&
1343+
IsOptionalPattern(schema.pattern) &&
1344+
IsOptionalFormat(schema.format)
13111345
)
13121346
}
13131347
/** Returns true if the given schema is TSymbol */
@@ -2106,6 +2140,61 @@ export namespace IndexedAccessor {
21062140
}
21072141
}
21082142
// --------------------------------------------------------------------------
2143+
// Intrinsic
2144+
// --------------------------------------------------------------------------
2145+
export namespace Intrinsic {
2146+
function Uncapitalize(value: string): string {
2147+
const [first, rest] = [value.slice(0, 1), value.slice(1)]
2148+
return `${first.toLowerCase()}${rest}`
2149+
}
2150+
function Capitalize(value: string): string {
2151+
const [first, rest] = [value.slice(0, 1), value.slice(1)]
2152+
return `${first.toUpperCase()}${rest}`
2153+
}
2154+
function Uppercase(value: string): string {
2155+
return value.toUpperCase()
2156+
}
2157+
function Lowercase(value: string): string {
2158+
return value.toLowerCase()
2159+
}
2160+
function IntrinsicTemplateLiteral(schema: TTemplateLiteral, mode: TIntrinsicMode) {
2161+
// note: template literals require special runtime handling as they are encoded in string patterns.
2162+
// This diverges from the mapped type which would otherwise map on the template literal kind.
2163+
const expression = TemplateLiteralParser.ParseExact(schema.pattern)
2164+
const finite = TemplateLiteralFinite.Check(expression)
2165+
if (!finite) return { ...schema, pattern: IntrinsicLiteral(schema.pattern, mode) } as any
2166+
const strings = [...TemplateLiteralGenerator.Generate(expression)]
2167+
const literals = strings.map((value) => Type.Literal(value))
2168+
const mapped = IntrinsicRest(literals as any, mode)
2169+
const union = Type.Union(mapped)
2170+
return Type.TemplateLiteral([union])
2171+
}
2172+
function IntrinsicLiteral(value: TLiteralValue, mode: TIntrinsicMode) {
2173+
// prettier-ignore
2174+
return typeof value === 'string' ? (
2175+
mode === 'Uncapitalize' ? Uncapitalize(value) :
2176+
mode === 'Capitalize' ? Capitalize(value) :
2177+
mode === 'Uppercase' ? Uppercase(value) :
2178+
mode === 'Lowercase' ? Lowercase(value) :
2179+
value) : ''
2180+
}
2181+
function IntrinsicRest(schema: TSchema[], mode: TIntrinsicMode): TSchema[] {
2182+
if (schema.length === 0) return []
2183+
const [L, ...R] = schema
2184+
return [Map(L, mode), ...IntrinsicRest(R, mode)]
2185+
}
2186+
function Visit(schema: TSchema, mode: TIntrinsicMode) {
2187+
if (TypeGuard.TTemplateLiteral(schema)) return IntrinsicTemplateLiteral(schema, mode)
2188+
if (TypeGuard.TUnion(schema)) return Type.Union(IntrinsicRest(schema.anyOf, mode))
2189+
if (TypeGuard.TLiteral(schema)) return Type.Literal(IntrinsicLiteral(schema.const, mode))
2190+
return schema
2191+
}
2192+
/** Applies an intrinsic string manipulation to the given type. */
2193+
export function Map<T extends TSchema, M extends TIntrinsicMode>(schema: T, mode: M): TIntrinsic<T, M> {
2194+
return Visit(schema, mode)
2195+
}
2196+
}
2197+
// --------------------------------------------------------------------------
21092198
// ObjectMap
21102199
// --------------------------------------------------------------------------
21112200
export namespace ObjectMap {
@@ -2538,10 +2627,9 @@ export class StandardTypeBuilder extends TypeBuilder {
25382627
public Boolean(options: SchemaOptions = {}): TBoolean {
25392628
return this.Create({ ...options, [Kind]: 'Boolean', type: 'boolean' })
25402629
}
2541-
/** `[Standard]` Capitalize a LiteralString type */
2542-
public Capitalize<T extends TLiteral<string>>(schema: T, options: SchemaOptions = {}): TLiteral<Capitalize<T['const']>> {
2543-
const [first, rest] = [schema.const.slice(0, 1), schema.const.slice(1)]
2544-
return Type.Literal(`${first.toUpperCase()}${rest}` as Capitalize<T['const']>, options)
2630+
/** `[Standard]` Intrinsic function to Capitalize LiteralString types */
2631+
public Capitalize<T extends TSchema>(schema: T, options: SchemaOptions = {}): TIntrinsic<T, 'Capitalize'> {
2632+
return { ...Intrinsic.Map(TypeClone.Clone(schema), 'Capitalize'), ...options }
25452633
}
25462634
/** `[Standard]` Creates a Composite object type */
25472635
public Composite<T extends TObject[]>(objects: [...T], options?: ObjectOptions): TComposite<T> {
@@ -2665,9 +2753,9 @@ export class StandardTypeBuilder extends TypeBuilder {
26652753
public Literal<T extends TLiteralValue>(value: T, options: SchemaOptions = {}): TLiteral<T> {
26662754
return this.Create({ ...options, [Kind]: 'Literal', const: value, type: typeof value as 'string' | 'number' | 'boolean' })
26672755
}
2668-
/** `[Standard]` Lowercase a LiteralString type */
2669-
public Lowercase<T extends TLiteral<string>>(schema: T, options: SchemaOptions = {}): TLiteral<Lowercase<T['const']>> {
2670-
return Type.Literal(schema.const.toLowerCase() as Lowercase<T['const']>, options)
2756+
/** `[Standard]` Intrinsic function to Lowercase LiteralString types */
2757+
public Lowercase<T extends TSchema>(schema: T, options: SchemaOptions = {}): TIntrinsic<T, 'Lowercase'> {
2758+
return { ...Intrinsic.Map(TypeClone.Clone(schema), 'Lowercase'), ...options }
26712759
}
26722760
/** `[Standard]` Creates a Never type */
26732761
public Never(options: SchemaOptions = {}): TNever {
@@ -2857,10 +2945,9 @@ export class StandardTypeBuilder extends TypeBuilder {
28572945
{ ...options, [Kind]: 'Tuple', type: 'array', minItems, maxItems }) as any
28582946
return this.Create(schema)
28592947
}
2860-
/** `[Standard]` Uncapitalize a LiteralString type */
2861-
public Uncapitalize<T extends TLiteral<string>>(schema: T, options: SchemaOptions = {}): TLiteral<Uncapitalize<T['const']>> {
2862-
const [first, rest] = [schema.const.slice(0, 1), schema.const.slice(1)]
2863-
return Type.Literal(`${first.toLocaleLowerCase()}${rest}` as Uncapitalize<T['const']>, options)
2948+
/** `[Standard]` Intrinsic function to Uncapitalize LiteralString types */
2949+
public Uncapitalize<T extends TSchema>(schema: T, options: SchemaOptions = {}): TIntrinsic<T, 'Uncapitalize'> {
2950+
return { ...Intrinsic.Map(TypeClone.Clone(schema), 'Uncapitalize'), ...options }
28642951
}
28652952
/** `[Standard]` Creates a Union type */
28662953
public Union(anyOf: [], options?: SchemaOptions): TNever
@@ -2890,9 +2977,9 @@ export class StandardTypeBuilder extends TypeBuilder {
28902977
public Unsafe<T>(options: UnsafeOptions = {}): TUnsafe<T> {
28912978
return this.Create({ ...options, [Kind]: options[Kind] || 'Unsafe' })
28922979
}
2893-
/** `[Standard]` Uppercase a LiteralString type */
2894-
public Uppercase<T extends TLiteral<string>>(schema: T, options: SchemaOptions = {}): TLiteral<Uppercase<T['const']>> {
2895-
return Type.Literal(schema.const.toUpperCase() as Uppercase<T['const']>, options)
2980+
/** `[Standard]` Intrinsic function to Uppercase LiteralString types */
2981+
public Uppercase<T extends TSchema>(schema: T, options: SchemaOptions = {}): TIntrinsic<T, 'Uppercase'> {
2982+
return { ...Intrinsic.Map(TypeClone.Clone(schema), 'Uppercase'), ...options }
28962983
}
28972984
}
28982985
// --------------------------------------------------------------------------

test/runtime/compiler/partial.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@ describe('type/compiler/Partial', () => {
1414
{ additionalProperties: false },
1515
)
1616
const T = Type.Partial(A)
17-
console.log(T)
18-
//Ok(T, { x: 1, y: 1, z: 1 })
19-
//Ok(T, { x: 1, y: 1 })
20-
//Ok(T, { x: 1 })
21-
//Ok(T, {})
17+
Ok(T, { x: 1, y: 1, z: 1 })
18+
Ok(T, { x: 1, y: 1 })
19+
Ok(T, { x: 1 })
20+
Ok(T, {})
2221
})
2322
it('Should update modifier types correctly when converting to partial', () => {
2423
const A = Type.Object(
@@ -38,7 +37,6 @@ describe('type/compiler/Partial', () => {
3837
strictEqual(T.properties.z[Optional], 'Optional')
3938
strictEqual(T.properties.w[Optional], 'Optional')
4039
})
41-
4240
it('Should inherit options from the source object', () => {
4341
const A = Type.Object(
4442
{

test/runtime/type/guard/capitalize.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1-
import { Type } from '@sinclair/typebox'
1+
import { TypeGuard, Type } from '@sinclair/typebox'
22
import { Assert } from '../../assert/index'
33

44
describe('type/guard/Capitalize', () => {
5-
it('Should guard for TCapitalize', () => {
5+
it('Should guard for Capitalize 1', () => {
6+
const T = Type.Capitalize(Type.Literal('hello'), { $id: 'hello', foo: 1 })
7+
Assert.IsTrue(TypeGuard.TLiteral(T))
8+
Assert.IsEqual(T.const, 'Hello')
9+
Assert.IsEqual(T.$id, 'hello')
10+
Assert.IsEqual(T.foo, 1)
11+
})
12+
it('Should guard for Capitalize 2', () => {
613
const T = Type.Capitalize(Type.Literal('hello'))
14+
Assert.IsTrue(TypeGuard.TLiteral(T))
715
Assert.IsEqual(T.const, 'Hello')
816
})
17+
it('Should guard for Capitalize 3', () => {
18+
const T = Type.Capitalize(Type.Union([Type.Literal('hello'), Type.Literal('world')]))
19+
Assert.IsTrue(TypeGuard.TUnion(T))
20+
Assert.IsEqual(T.anyOf[0].const, 'Hello')
21+
Assert.IsEqual(T.anyOf[1].const, 'World')
22+
})
23+
it('Should guard for Capitalize 4', () => {
24+
const T = Type.Capitalize(Type.TemplateLiteral('hello${0|1}'))
25+
Assert.IsTrue(TypeGuard.TTemplateLiteral(T))
26+
Assert.IsEqual(T.pattern, '^(Hello0|Hello1)$')
27+
})
928
})

0 commit comments

Comments
 (0)