Skip to content

Commit 1190b10

Browse files
committed
feat(type): support intrinsics Capitalize, Uppercase, Lowercase, Uncapitalize
supports now expressions like these ```typescript type A = `Prefix_${Uppercase<'hello' | 'world'>}_Suffix` ``` which will be expanded to ```typescript type T = 'Prefix_HELLO_Suffix' | 'Prefix_WORLD_Suffix' ```
1 parent afbf6e2 commit 1190b10

File tree

6 files changed

+228
-27
lines changed

6 files changed

+228
-27
lines changed

packages/type-compiler/src/compiler.ts

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ import {
9595
serializeEntityNameAsExpression,
9696
} from './reflection-ast.js';
9797
import { SourceFile } from './ts-types.js';
98-
import { MappedModifier, ReflectionOp, TypeNumberBrand } from '@deepkit/type-spec';
98+
import { MappedModifier, ReflectionOp, TypeIntrinsic, TypeNumberBrand } from '@deepkit/type-spec';
9999
import { Resolver } from './resolver.js';
100100
import { knownLibFilesForCompilerOptions } from '@typescript/vfs';
101101
import { debug, debug2 } from './debug.js';
@@ -1992,6 +1992,34 @@ export class ReflectionTransformer implements CustomTransformer {
19921992
}
19931993
break;
19941994
}
1995+
case SyntaxKind.IntrinsicKeyword: {
1996+
if (node.parent?.kind !== SyntaxKind.TypeAliasDeclaration) {
1997+
program.pushOp(ReflectionOp.never);
1998+
break;
1999+
}
2000+
const parent = node.parent as TypeAliasDeclaration;
2001+
const T = parent.typeParameters?.[0];
2002+
// All intrinsics require one type parameter
2003+
if (!T) {
2004+
program.pushOp(ReflectionOp.never);
2005+
break;
2006+
}
2007+
const name = getNameAsString(parent.name);
2008+
const mapping: Record<string, TypeIntrinsic> = {
2009+
'Capitalize': TypeIntrinsic.Capitalize,
2010+
'Uppercase': TypeIntrinsic.Uppercase,
2011+
'Lowercase': TypeIntrinsic.Lowercase,
2012+
'Uncapitalize': TypeIntrinsic.Uncapitalize,
2013+
};
2014+
const intrinsic = mapping[name];
2015+
if (intrinsic === undefined) {
2016+
program.pushOp(ReflectionOp.never);
2017+
break;
2018+
}
2019+
this.extractPackStructOfTypeReference(T.name, program);
2020+
program.pushOp(ReflectionOp.intrinsic, Number(intrinsic));
2021+
break;
2022+
}
19952023
default: {
19962024
program.pushOp(ReflectionOp.never);
19972025
}
@@ -2147,15 +2175,21 @@ export class ReflectionTransformer implements CustomTransformer {
21472175
return res === 'never';
21482176
}
21492177

2150-
protected extractPackStructOfTypeReference(type: TypeReferenceNode | ExpressionWithTypeArguments, program: CompilerProgram): void {
2151-
const typeName: EntityName | undefined = isTypeReferenceNode(type) ? type.typeName : (isIdentifier(type.expression) ? type.expression : undefined);
2178+
protected extractPackStructOfTypeReference(type: Identifier | TypeReferenceNode | ExpressionWithTypeArguments, program: CompilerProgram): void {
2179+
const typeName: EntityName | undefined = isIdentifier(type)
2180+
? type
2181+
: isTypeReferenceNode(type)
2182+
? type.typeName
2183+
: (isIdentifier(type.expression) ? type.expression : undefined);
2184+
const typeArguments: readonly TypeNode[] | undefined = isTypeReferenceNode(type) || isExpressionWithTypeArguments(type) ? type.typeArguments : [];
2185+
21522186
if (!typeName) {
21532187
program.pushOp(ReflectionOp.any);
21542188
return;
21552189
}
21562190

2157-
if (isIdentifier(typeName) && getIdentifierName(typeName) === 'InlineRuntimeType' && type.typeArguments && type.typeArguments[0] && isTypeQueryNode(type.typeArguments[0])) {
2158-
const expression = serializeEntityNameAsExpression(this.f, type.typeArguments[0].exprName);
2191+
if (isIdentifier(typeName) && getIdentifierName(typeName) === 'InlineRuntimeType' && typeArguments && typeArguments[0] && isTypeQueryNode(typeArguments[0])) {
2192+
const expression = serializeEntityNameAsExpression(this.f, typeArguments[0].exprName);
21592193
program.pushOp(ReflectionOp.arg, program.pushStack(expression));
21602194
return;
21612195
}
@@ -2166,8 +2200,8 @@ export class ReflectionTransformer implements CustomTransformer {
21662200
program.pushOp(op);
21672201
} else if (isIdentifier(typeName) && getIdentifierName(typeName) === 'Promise') {
21682202
//promise has always one sub type
2169-
if (type.typeArguments && type.typeArguments[0]) {
2170-
this.extractPackStructOfType(type.typeArguments[0], program);
2203+
if (typeArguments && typeArguments[0]) {
2204+
this.extractPackStructOfType(typeArguments[0], program);
21712205
} else {
21722206
program.pushOp(ReflectionOp.any);
21732207
}
@@ -2263,8 +2297,8 @@ export class ReflectionTransformer implements CustomTransformer {
22632297
//Set/Map are interface declarations
22642298
const name = getNameAsString(typeName);
22652299
if (name === 'Array') {
2266-
if (type.typeArguments && type.typeArguments[0]) {
2267-
this.extractPackStructOfType(type.typeArguments[0], program);
2300+
if (typeArguments && typeArguments[0]) {
2301+
this.extractPackStructOfType(typeArguments[0], program);
22682302
} else {
22692303
program.pushOp(ReflectionOp.any);
22702304
}
@@ -2278,21 +2312,21 @@ export class ReflectionTransformer implements CustomTransformer {
22782312
program.popFrameImplicit();
22792313
return;
22802314
} else if (name === 'Set') {
2281-
if (type.typeArguments && type.typeArguments[0]) {
2282-
this.extractPackStructOfType(type.typeArguments[0], program);
2315+
if (typeArguments && typeArguments[0]) {
2316+
this.extractPackStructOfType(typeArguments[0], program);
22832317
} else {
22842318
program.pushOp(ReflectionOp.any);
22852319
}
22862320
program.pushOp(ReflectionOp.set);
22872321
return;
22882322
} else if (name === 'Map') {
2289-
if (type.typeArguments && type.typeArguments[0]) {
2290-
this.extractPackStructOfType(type.typeArguments[0], program);
2323+
if (typeArguments && typeArguments[0]) {
2324+
this.extractPackStructOfType(typeArguments[0], program);
22912325
} else {
22922326
program.pushOp(ReflectionOp.any);
22932327
}
2294-
if (type.typeArguments && type.typeArguments[1]) {
2295-
this.extractPackStructOfType(type.typeArguments[1], program);
2328+
if (typeArguments && typeArguments[1]) {
2329+
this.extractPackStructOfType(typeArguments[1], program);
22962330
} else {
22972331
program.pushOp(ReflectionOp.any);
22982332
}
@@ -2386,20 +2420,20 @@ export class ReflectionTransformer implements CustomTransformer {
23862420
const index = program.pushStack(
23872421
program.forNode === declaration ? 0 : this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, runtimeTypeName),
23882422
);
2389-
if (type.typeArguments) {
2390-
for (const argument of type.typeArguments) {
2423+
if (typeArguments) {
2424+
for (const argument of typeArguments) {
23912425
this.extractPackStructOfType(argument, program);
23922426
}
2393-
program.pushOp(ReflectionOp.inlineCall, index, type.typeArguments.length);
2427+
program.pushOp(ReflectionOp.inlineCall, index, typeArguments.length);
23942428
} else {
23952429
program.pushOp(ReflectionOp.inline, index);
23962430
}
23972431

2398-
// if (type.typeArguments) {
2399-
// for (const argument of type.typeArguments) {
2432+
// if (typeArguments) {
2433+
// for (const argument of typeArguments) {
24002434
// this.extractPackStructOfType(argument, program);
24012435
// }
2402-
// program.pushOp(ReflectionOp.inlineCall, index, type.typeArguments.length);
2436+
// program.pushOp(ReflectionOp.inlineCall, index, typeArguments.length);
24032437
// } else {
24042438
// program.pushOp(ReflectionOp.inline, index);
24052439
// }
@@ -2430,8 +2464,8 @@ export class ReflectionTransformer implements CustomTransformer {
24302464

24312465
if (resolved.importDeclaration && isIdentifier(typeName)) ensureImportIsEmitted(resolved.importDeclaration, typeName);
24322466
program.pushFrame();
2433-
if (type.typeArguments) {
2434-
for (const typeArgument of type.typeArguments) {
2467+
if (typeArguments) {
2468+
for (const typeArgument of typeArguments) {
24352469
this.extractPackStructOfType(typeArgument, program);
24362470
}
24372471
}
@@ -2519,7 +2553,7 @@ export class ReflectionTransformer implements CustomTransformer {
25192553
* }
25202554
* ```
25212555
*/
2522-
protected needsToBeInferred(declaration: TypeParameterDeclaration, type: TypeReferenceNode | ExpressionWithTypeArguments): boolean {
2556+
protected needsToBeInferred(declaration: TypeParameterDeclaration, type: Identifier | TypeReferenceNode | ExpressionWithTypeArguments): boolean {
25232557
const declarationUser = this.getTypeUser(declaration);
25242558
const typeUser = this.getTypeUser(type);
25252559

@@ -2539,7 +2573,7 @@ export class ReflectionTransformer implements CustomTransformer {
25392573
program.pushOp(ReflectionOp.typeName, program.findOrAddStackEntry(typeName));
25402574
}
25412575

2542-
protected resolveTypeParameter(declaration: TypeParameterDeclaration, type: TypeReferenceNode | ExpressionWithTypeArguments, program: CompilerProgram) {
2576+
protected resolveTypeParameter(declaration: TypeParameterDeclaration, type: Identifier | TypeReferenceNode | ExpressionWithTypeArguments, program: CompilerProgram) {
25432577
//check if `type` was used in an expression. if so, we need to resolve it from runtime, otherwise we mark it as T
25442578
const isUsedInFunction = isFunctionLike(declaration.parent);
25452579
const resolveRuntimeTypeParameter = (isUsedInFunction && program.isResolveFunctionParameters(declaration.parent)) || (this.needsToBeInferred(declaration, type));

packages/type-compiler/tests/transpile.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,3 +592,13 @@ test('infer type', () => {
592592
console.log(res.app);
593593
expect(res.app).toContain(`'a'`);
594594
});
595+
596+
test('intrinsic type', () => {
597+
const res = transpile({
598+
'app': `
599+
export type A = Capitalize<'a'>;
600+
`
601+
});
602+
console.log(res.app);
603+
expect(res.app).toContain(`const __ΩCapitalize = `);
604+
});

packages/type-spec/src/type.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ export enum TypeNumberBrand {
3535
float64,
3636
}
3737

38+
export enum TypeIntrinsic {
39+
Uppercase,
40+
Lowercase,
41+
Capitalize,
42+
Uncapitalize
43+
}
44+
3845
/**
3946
* The instruction set.
4047
* Should not be greater than 93 members, because we encode it via charCode starting at 33. +93 means we end up with charCode=126
@@ -236,4 +243,6 @@ export enum ReflectionOp {
236243
implements, //pops one type from the stack and assigns it to the latest class on the stack as `implements` type.
237244

238245
nominal, //marks the last type on the stack as nominal. (used at the end of class/interface/type alias programs).
246+
247+
intrinsic, //has one parameter, the kind of the intrinsic type, and pulls a type from the stack.
239248
}

packages/type/src/reflection/processor.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import {
5353
validationAnnotation,
5454
widenLiteral,
5555
} from './type.js';
56-
import { MappedModifier, ReflectionOp } from '@deepkit/type-spec';
56+
import { MappedModifier, ReflectionOp, TypeIntrinsic } from '@deepkit/type-spec';
5757
import { isExtendable } from './extends.js';
5858
import { ClassType, isArray, isClass, isFunction, stringifyValueWithType } from '@deepkit/core';
5959
import { isWithDeferredDecorators } from '../decorator.js';
@@ -1186,6 +1186,30 @@ export class Processor {
11861186
t.id = state.nominalId++;
11871187
break;
11881188
}
1189+
case ReflectionOp.intrinsic: {
1190+
// intrinsics operate on the current stack entry
1191+
const type = this.pop() as Type;
1192+
const kind = this.eatParameter() as TypeIntrinsic;
1193+
switch (kind) {
1194+
case TypeIntrinsic.Uppercase: {
1195+
this.push(handleUppercase(type));
1196+
break;
1197+
}
1198+
case TypeIntrinsic.Lowercase: {
1199+
this.push(handleLowercase(type));
1200+
break;
1201+
}
1202+
case TypeIntrinsic.Capitalize: {
1203+
this.push(handleCapitalize(type));
1204+
break;
1205+
}
1206+
case TypeIntrinsic.Uncapitalize: {
1207+
this.push(handleUncapitalize(type));
1208+
break;
1209+
}
1210+
}
1211+
break;
1212+
}
11891213
case ReflectionOp.inline: {
11901214
const pPosition = this.eatParameter() as number;
11911215
const pOrFn = program.stack[pPosition] as number | Packed | (() => Packed);
@@ -1419,7 +1443,7 @@ export class Processor {
14191443

14201444
// two different primitives always return never
14211445
if (isPrimitive(a) && isPrimitive(b) && a.kind !== b.kind) {
1422-
return { kind: ReflectionKind.never }
1446+
return { kind: ReflectionKind.never };
14231447
}
14241448

14251449
if (a.kind === ReflectionKind.objectLiteral || a.kind === ReflectionKind.class || a.kind === ReflectionKind.never || a.kind === ReflectionKind.unknown) return b;
@@ -1766,6 +1790,46 @@ export class Processor {
17661790
}
17671791
}
17681792

1793+
function handleUppercase(type: Type): Type {
1794+
if (type.kind === ReflectionKind.union) {
1795+
return { kind: ReflectionKind.union, types: type.types.map(t => handleUppercase(t)) };
1796+
}
1797+
if (type.kind !== ReflectionKind.literal || 'string' !== typeof type.literal) {
1798+
return { kind: ReflectionKind.string };
1799+
}
1800+
return { kind: ReflectionKind.literal, literal: type.literal.toUpperCase() };
1801+
}
1802+
1803+
function handleLowercase(type: Type): Type {
1804+
if (type.kind === ReflectionKind.union) {
1805+
return { kind: ReflectionKind.union, types: type.types.map(t => handleLowercase(t)) };
1806+
}
1807+
if (type.kind !== ReflectionKind.literal || 'string' !== typeof type.literal) {
1808+
return { kind: ReflectionKind.string };
1809+
}
1810+
return { kind: ReflectionKind.literal, literal: type.literal.toLowerCase() };
1811+
}
1812+
1813+
function handleCapitalize(type: Type): Type {
1814+
if (type.kind === ReflectionKind.union) {
1815+
return { kind: ReflectionKind.union, types: type.types.map(t => handleCapitalize(t)) };
1816+
}
1817+
if (type.kind !== ReflectionKind.literal || 'string' !== typeof type.literal) {
1818+
return { kind: ReflectionKind.string };
1819+
}
1820+
return { kind: ReflectionKind.literal, literal: type.literal.charAt(0).toUpperCase() + type.literal.slice(1) };
1821+
}
1822+
1823+
function handleUncapitalize(type: Type): Type {
1824+
if (type.kind === ReflectionKind.union) {
1825+
return { kind: ReflectionKind.union, types: type.types.map(t => handleUncapitalize(t)) };
1826+
}
1827+
if (type.kind !== ReflectionKind.literal || 'string' !== typeof type.literal) {
1828+
return { kind: ReflectionKind.string };
1829+
}
1830+
return { kind: ReflectionKind.literal, literal: type.literal.charAt(0).toLowerCase() + type.literal.slice(1) };
1831+
}
1832+
17691833
function typeInferFromContainer(container: Iterable<any>): Type {
17701834
const union: TypeUnion = { kind: ReflectionKind.union, types: [] };
17711835
for (const item of container) {

packages/type/src/reflection/type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2523,6 +2523,7 @@ export function stringifyType(type: Type, stateIn: Partial<StringifyTypeOptions>
25232523
break;
25242524
case ReflectionKind.templateLiteral:
25252525
stack.push({ before: '`' });
2526+
debugger;
25262527
for (let i = type.types.length - 1; i >= 0; i--) {
25272528
const sub = type.types[i];
25282529
if (sub.kind === ReflectionKind.literal) {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { expect, test } from '@jest/globals';
2+
import { typeOf } from '../src/reflection/reflection.js';
3+
import { assertType, ReflectionKind, stringifyResolvedType } from '../src/reflection/type.js';
4+
5+
test('Capitalize', () => {
6+
type A = Capitalize<'hello world'>;
7+
const type = typeOf<A>();
8+
assertType(type, ReflectionKind.literal);
9+
expect(type.literal).toBe('Hello world');
10+
});
11+
12+
test('Uppercase', () => {
13+
type A = Uppercase<'hello world'>;
14+
const type = typeOf<A>();
15+
assertType(type, ReflectionKind.literal);
16+
expect(type.literal).toBe('HELLO WORLD');
17+
});
18+
19+
test('Lowercase', () => {
20+
type A = Lowercase<'HELLO WORLD'>;
21+
const type = typeOf<A>();
22+
assertType(type, ReflectionKind.literal);
23+
expect(type.literal).toBe('hello world');
24+
});
25+
26+
test('Uncapitalize', () => {
27+
type A = Uncapitalize<'Hello World'>;
28+
const type = typeOf<A>();
29+
assertType(type, ReflectionKind.literal);
30+
expect(type.literal).toBe('hello World');
31+
});
32+
33+
test('template literal with intrinsic', () => {
34+
type A = `Prefix_${Uppercase<'hello'>}_Suffix`;
35+
const type = typeOf<A>();
36+
assertType(type, ReflectionKind.literal);
37+
expect(type.literal).toBe('Prefix_HELLO_Suffix');
38+
});
39+
40+
test('union intrinsic', () => {
41+
type A = Uppercase<'hello' | 'world'>;
42+
const type = typeOf<A>();
43+
expect(stringifyResolvedType(type)).toBe(`'HELLO' | 'WORLD'`);
44+
assertType(type, ReflectionKind.union);
45+
expect(type.types).toHaveLength(2);
46+
assertType(type.types[0], ReflectionKind.literal);
47+
assertType(type.types[1], ReflectionKind.literal);
48+
expect(type.types[0].literal).toBe('HELLO');
49+
expect(type.types[1].literal).toBe('WORLD');
50+
});
51+
52+
test('template literal unioning intrinsic', () => {
53+
type A = `Prefix_${Uppercase<'hello' | 'world'>}_Suffix`;
54+
const type = typeOf<A>();
55+
expect(stringifyResolvedType(type)).toBe(`'Prefix_HELLO_Suffix' | 'Prefix_WORLD_Suffix'`);
56+
assertType(type, ReflectionKind.union);
57+
expect(type.types).toHaveLength(2);
58+
assertType(type.types[0], ReflectionKind.literal);
59+
assertType(type.types[1], ReflectionKind.literal);
60+
expect(type.types[0].literal).toBe('Prefix_HELLO_Suffix');
61+
expect(type.types[1].literal).toBe('Prefix_WORLD_Suffix');
62+
});
63+
64+
test('complex 1', () => {
65+
type Keys = 'health' | 'damage' | 'defense' | 'clickRadius';
66+
type Modified = `original${Capitalize<Keys>}`;
67+
type Attributes = {
68+
[key in Keys | Modified]: number;
69+
};
70+
const type = typeOf<Modified>();
71+
expect(stringifyResolvedType(type)).toBe(`'originalHealth' | 'originalDamage' | 'originalDefense' | 'originalClickRadius'`);
72+
const attributes = typeOf<Attributes>();
73+
expect(stringifyResolvedType(attributes)).toBe(`Attributes {
74+
health: number;
75+
damage: number;
76+
defense: number;
77+
clickRadius: number;
78+
originalHealth: number;
79+
originalDamage: number;
80+
originalDefense: number;
81+
originalClickRadius: number;
82+
}`);
83+
});

0 commit comments

Comments
 (0)