Skip to content

Commit ffd1bc7

Browse files
authored
feat(type): support intrinsics Capitalize, Uppercase, Lowercase, Uncapitalize (#681)
* 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 ffd1bc7

File tree

5 files changed

+239
-27
lines changed

5 files changed

+239
-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 : undefined;
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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ export enum TypeNumberBrand {
3535
float64,
3636
}
3737

38+
/**
39+
* Intrinsic string manipulation type operations.
40+
* These correspond to TypeScript's built-in string manipulation utility types.
41+
*/
42+
export enum TypeIntrinsic {
43+
Uppercase,
44+
Lowercase,
45+
Capitalize,
46+
Uncapitalize
47+
}
48+
3849
/**
3950
* The instruction set.
4051
* 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 +247,6 @@ export enum ReflectionOp {
236247
implements, //pops one type from the stack and assigns it to the latest class on the stack as `implements` type.
237248

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

packages/type/src/reflection/processor.ts

Lines changed: 74 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,34 @@ 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+
default: {
1211+
this.push(type);
1212+
break;
1213+
}
1214+
}
1215+
break;
1216+
}
11891217
case ReflectionOp.inline: {
11901218
const pPosition = this.eatParameter() as number;
11911219
const pOrFn = program.stack[pPosition] as number | Packed | (() => Packed);
@@ -1419,7 +1447,7 @@ export class Processor {
14191447

14201448
// two different primitives always return never
14211449
if (isPrimitive(a) && isPrimitive(b) && a.kind !== b.kind) {
1422-
return { kind: ReflectionKind.never }
1450+
return { kind: ReflectionKind.never };
14231451
}
14241452

14251453
if (a.kind === ReflectionKind.objectLiteral || a.kind === ReflectionKind.class || a.kind === ReflectionKind.never || a.kind === ReflectionKind.unknown) return b;
@@ -1766,6 +1794,50 @@ export class Processor {
17661794
}
17671795
}
17681796

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

0 commit comments

Comments
 (0)