just-the-type 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/decorators.d.ts +22 -0
- package/dist/decorators.js +42 -0
- package/dist/emitter.d.ts +2 -0
- package/dist/emitter.js +324 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +13 -0
- package/dist/lib.d.ts +3 -0
- package/dist/lib.js +11 -0
- package/lib/main.tsp +21 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Kneip
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# just-the-type
|
|
2
|
+
|
|
3
|
+
Emitter to create TypeScript types from TypeSpec. No clients, no runtime, no
|
|
4
|
+
serializers — just the types, built with the TypeScript compiler factory API.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
pnpm add just-the-type
|
|
10
|
+
tsp compile . --emit just-the-type
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The emitter writes a single `types.ts` to `tsp-output/just-the-type/`.
|
|
14
|
+
|
|
15
|
+
## What gets emitted
|
|
16
|
+
|
|
17
|
+
| TypeSpec | TypeScript |
|
|
18
|
+
| ---------------------------------------- | ---------------------------------------------- |
|
|
19
|
+
| `model` | `export interface` (with `extends`) |
|
|
20
|
+
| `model Wrapper<T>` (constraints/defaults) | generic `export interface Wrapper<T>` |
|
|
21
|
+
| `model Pets is Pet[]` | `export type Pets = Pet[]` |
|
|
22
|
+
| `...Record<T>` spread | index signature `[key: string]: T` |
|
|
23
|
+
| `enum` (incl. spread) | `export enum` |
|
|
24
|
+
| named `union` (incl. generic) | `export type` alias |
|
|
25
|
+
| `op` | `export type` function alias |
|
|
26
|
+
| `interface` | `export interface` with method signatures |
|
|
27
|
+
| custom `scalar` | `export type` alias to its base primitive |
|
|
28
|
+
| doc comments / `@doc` | JSDoc comments |
|
|
29
|
+
| string templates `"a-${string}"` | template literal types |
|
|
30
|
+
| anonymous models, unions, intersections | inlined structurally |
|
|
31
|
+
| templates with `valueof` parameters | declaration skipped, instantiations inlined |
|
|
32
|
+
| `int64` / `uint64` | `bigint` |
|
|
33
|
+
| other numerics | `number` |
|
|
34
|
+
| `bytes` | `Uint8Array` |
|
|
35
|
+
| dates, times, `duration`, `url` | `string` |
|
|
36
|
+
|
|
37
|
+
Not represented in the output: `alias` declarations (dissolved by the TypeSpec
|
|
38
|
+
checker, their targets are inlined), values/`const`, and API-metadata decorators
|
|
39
|
+
such as `@visibility`, `@format`, or `@encode` that do not change the type shape.
|
|
40
|
+
|
|
41
|
+
## Decorators
|
|
42
|
+
|
|
43
|
+
The library ships decorators for TypeScript features that TypeSpec cannot
|
|
44
|
+
express. Import the library and bring them into scope:
|
|
45
|
+
|
|
46
|
+
```typespec
|
|
47
|
+
import "just-the-type";
|
|
48
|
+
using JustTheType;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| Decorator | Target | Effect |
|
|
52
|
+
| ------------------------------- | -------------------------- | ------------------------------------------------------------ |
|
|
53
|
+
| `@promise` | `op`, `interface` | Wraps return types in `Promise<T>` |
|
|
54
|
+
| `@readonly` | property, `model` | Emits the `readonly` modifier |
|
|
55
|
+
| `@tsType("Date")` | `scalar`, property | Replaces the emitted type with raw TypeScript |
|
|
56
|
+
| `@tsType("Dayjs", "dayjs")` | `scalar`, property | Same, plus `import type { Dayjs } from "dayjs";` |
|
|
57
|
+
| `@literalUnion` | `enum` | Emits `type Color = "red" \| "blue"` instead of a TS enum |
|
|
58
|
+
|
|
59
|
+
`@tsType` also works on built-in scalars via augment decorators, e.g. map all
|
|
60
|
+
`utcDateTime` to real `Date` objects:
|
|
61
|
+
|
|
62
|
+
```typespec
|
|
63
|
+
@@tsType(utcDateTime, "Date");
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For qualified overrides such as `@tsType("Temporal.Instant", "@js-temporal/polyfill")`
|
|
67
|
+
the root identifier (`Temporal`) is imported.
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
pnpm install
|
|
73
|
+
pnpm test # vitest
|
|
74
|
+
pnpm build # tsc -> dist/
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Try it on the sample:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
pnpm sample # tsp compile sample -> sample/tsp-output/
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DecoratorContext, Enum, Interface, Model, ModelProperty, Operation, Program, Scalar, Type } from "@typespec/compiler";
|
|
2
|
+
export interface TsTypeOverride {
|
|
3
|
+
type: string;
|
|
4
|
+
from?: string;
|
|
5
|
+
}
|
|
6
|
+
declare function $promise(context: DecoratorContext, target: Operation | Interface): void;
|
|
7
|
+
declare function $readonly(context: DecoratorContext, target: ModelProperty | Model): void;
|
|
8
|
+
declare function $tsType(context: DecoratorContext, target: Scalar | ModelProperty, type: string, importFrom?: string): void;
|
|
9
|
+
declare function $literalUnion(context: DecoratorContext, target: Enum): void;
|
|
10
|
+
export declare const $decorators: {
|
|
11
|
+
JustTheType: {
|
|
12
|
+
promise: typeof $promise;
|
|
13
|
+
readonly: typeof $readonly;
|
|
14
|
+
tsType: typeof $tsType;
|
|
15
|
+
literalUnion: typeof $literalUnion;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare function isAsync(program: Program, type: Type): boolean;
|
|
19
|
+
export declare function isReadonly(program: Program, type: Type): boolean;
|
|
20
|
+
export declare function getTsType(program: Program, type: Type): TsTypeOverride | undefined;
|
|
21
|
+
export declare function isLiteralUnion(program: Program, type: Type): boolean;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { $lib } from "./lib.js";
|
|
2
|
+
const keys = $lib.stateKeys;
|
|
3
|
+
// Decorator arguments declared as `valueof string` arrive as plain strings;
|
|
4
|
+
// without the extern declaration (e.g. in tests) they arrive as StringLiteral types.
|
|
5
|
+
function asString(value) {
|
|
6
|
+
return typeof value === "object" ? value.value : value;
|
|
7
|
+
}
|
|
8
|
+
function $promise(context, target) {
|
|
9
|
+
context.program.stateSet(keys.async).add(target);
|
|
10
|
+
}
|
|
11
|
+
function $readonly(context, target) {
|
|
12
|
+
context.program.stateSet(keys.readonly).add(target);
|
|
13
|
+
}
|
|
14
|
+
function $tsType(context, target, type, importFrom) {
|
|
15
|
+
context.program.stateMap(keys.tsType).set(target, {
|
|
16
|
+
type: asString(type),
|
|
17
|
+
from: asString(importFrom),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function $literalUnion(context, target) {
|
|
21
|
+
context.program.stateSet(keys.literalUnion).add(target);
|
|
22
|
+
}
|
|
23
|
+
export const $decorators = {
|
|
24
|
+
JustTheType: {
|
|
25
|
+
promise: $promise,
|
|
26
|
+
readonly: $readonly,
|
|
27
|
+
tsType: $tsType,
|
|
28
|
+
literalUnion: $literalUnion,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export function isAsync(program, type) {
|
|
32
|
+
return program.stateSet(keys.async).has(type);
|
|
33
|
+
}
|
|
34
|
+
export function isReadonly(program, type) {
|
|
35
|
+
return program.stateSet(keys.readonly).has(type);
|
|
36
|
+
}
|
|
37
|
+
export function getTsType(program, type) {
|
|
38
|
+
return program.stateMap(keys.tsType).get(type);
|
|
39
|
+
}
|
|
40
|
+
export function isLiteralUnion(program, type) {
|
|
41
|
+
return program.stateSet(keys.literalUnion).has(type);
|
|
42
|
+
}
|
package/dist/emitter.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { getDoc, isArrayModelType, isRecordModelType, isTemplateDeclaration, } from "@typespec/compiler";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { getTsType, isAsync, isLiteralUnion, isReadonly } from "./decorators.js";
|
|
4
|
+
const f = ts.factory;
|
|
5
|
+
const stringType = () => f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
6
|
+
const numberType = () => f.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
|
|
7
|
+
const bigintType = () => f.createKeywordTypeNode(ts.SyntaxKind.BigIntKeyword);
|
|
8
|
+
const unknownType = () => f.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
9
|
+
const scalarMap = {
|
|
10
|
+
string: stringType,
|
|
11
|
+
boolean: () => f.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword),
|
|
12
|
+
bytes: () => f.createTypeReferenceNode("Uint8Array"),
|
|
13
|
+
int64: bigintType,
|
|
14
|
+
uint64: bigintType,
|
|
15
|
+
numeric: numberType,
|
|
16
|
+
integer: numberType,
|
|
17
|
+
float: numberType,
|
|
18
|
+
decimal: numberType,
|
|
19
|
+
decimal128: numberType,
|
|
20
|
+
float32: numberType,
|
|
21
|
+
float64: numberType,
|
|
22
|
+
int32: numberType,
|
|
23
|
+
int16: numberType,
|
|
24
|
+
int8: numberType,
|
|
25
|
+
safeint: numberType,
|
|
26
|
+
uint32: numberType,
|
|
27
|
+
uint16: numberType,
|
|
28
|
+
uint8: numberType,
|
|
29
|
+
url: stringType,
|
|
30
|
+
plainDate: stringType,
|
|
31
|
+
plainTime: stringType,
|
|
32
|
+
utcDateTime: stringType,
|
|
33
|
+
offsetDateTime: stringType,
|
|
34
|
+
duration: stringType,
|
|
35
|
+
};
|
|
36
|
+
const exportModifier = () => [f.createModifier(ts.SyntaxKind.ExportKeyword)];
|
|
37
|
+
export function emitTypes(program) {
|
|
38
|
+
const ctx = { program, imports: new Map() };
|
|
39
|
+
const statements = [];
|
|
40
|
+
collectNamespace(ctx, program.getGlobalNamespaceType(), statements);
|
|
41
|
+
statements.unshift(...importDeclarations(ctx));
|
|
42
|
+
const file = ts.createSourceFile("types.ts", "", ts.ScriptTarget.ES2022, false, ts.ScriptKind.TS);
|
|
43
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
44
|
+
return printer.printList(ts.ListFormat.MultiLine, f.createNodeArray(statements), file);
|
|
45
|
+
}
|
|
46
|
+
function importDeclarations(ctx) {
|
|
47
|
+
return [...ctx.imports].map(([module, names]) => f.createImportDeclaration(undefined, f.createImportClause(ts.SyntaxKind.TypeKeyword, undefined, f.createNamedImports([...names].sort().map((name) => f.createImportSpecifier(false, undefined, f.createIdentifier(name))))), f.createStringLiteral(module)));
|
|
48
|
+
}
|
|
49
|
+
function collectNamespace(ctx, ns, statements) {
|
|
50
|
+
for (const model of ns.models.values()) {
|
|
51
|
+
if (isTemplateDeclaration(model) && !canEmitGeneric(ctx, model))
|
|
52
|
+
continue;
|
|
53
|
+
statements.push(modelDeclaration(ctx, model));
|
|
54
|
+
}
|
|
55
|
+
for (const scalar of ns.scalars.values()) {
|
|
56
|
+
if (!isTemplateDeclaration(scalar))
|
|
57
|
+
statements.push(scalarDeclaration(ctx, scalar));
|
|
58
|
+
}
|
|
59
|
+
for (const en of ns.enums.values())
|
|
60
|
+
statements.push(enumDeclaration(ctx, en));
|
|
61
|
+
for (const union of ns.unions.values()) {
|
|
62
|
+
if (!union.name)
|
|
63
|
+
continue;
|
|
64
|
+
if (isTemplateDeclaration(union) && !canEmitGeneric(ctx, union))
|
|
65
|
+
continue;
|
|
66
|
+
statements.push(withDoc(ctx, union, f.createTypeAliasDeclaration(exportModifier(), union.name, typeParameterDeclarations(ctx, union), unionToNode(ctx, union))));
|
|
67
|
+
}
|
|
68
|
+
for (const op of ns.operations.values()) {
|
|
69
|
+
if (isTemplateDeclaration(op))
|
|
70
|
+
continue;
|
|
71
|
+
statements.push(withDoc(ctx, op, f.createTypeAliasDeclaration(exportModifier(), op.name, undefined, f.createFunctionTypeNode(undefined, operationParameters(ctx, op), returnTypeNode(ctx, op)))));
|
|
72
|
+
}
|
|
73
|
+
for (const iface of ns.interfaces.values()) {
|
|
74
|
+
if (!isTemplateDeclaration(iface))
|
|
75
|
+
statements.push(interfaceDeclaration(ctx, iface));
|
|
76
|
+
}
|
|
77
|
+
for (const child of ns.namespaces.values()) {
|
|
78
|
+
if (child.name === "TypeSpec" && !child.namespace?.namespace)
|
|
79
|
+
continue;
|
|
80
|
+
collectNamespace(ctx, child, statements);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function modelDeclaration(ctx, model) {
|
|
84
|
+
const typeParameters = typeParameterDeclarations(ctx, model);
|
|
85
|
+
if (isArrayModelType(model)) {
|
|
86
|
+
return withDoc(ctx, model, f.createTypeAliasDeclaration(exportModifier(), model.name, typeParameters, f.createArrayTypeNode(typeToNode(ctx, model.indexer.value))));
|
|
87
|
+
}
|
|
88
|
+
const heritage = model.baseModel && namedReference(ctx, model.baseModel);
|
|
89
|
+
return withDoc(ctx, model, f.createInterfaceDeclaration(exportModifier(), model.name, typeParameters, heritage
|
|
90
|
+
? [
|
|
91
|
+
f.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [
|
|
92
|
+
f.createExpressionWithTypeArguments(f.createIdentifier(heritage.name), heritage.args),
|
|
93
|
+
]),
|
|
94
|
+
]
|
|
95
|
+
: undefined, modelMembers(ctx, model)));
|
|
96
|
+
}
|
|
97
|
+
function modelMembers(ctx, model) {
|
|
98
|
+
const members = [...model.properties.values()].map((prop) => withDoc(ctx, prop, f.createPropertySignature(isReadonly(ctx.program, prop) || isReadonly(ctx.program, model)
|
|
99
|
+
? [f.createModifier(ts.SyntaxKind.ReadonlyKeyword)]
|
|
100
|
+
: undefined, propertyName(prop.name), prop.optional ? f.createToken(ts.SyntaxKind.QuestionToken) : undefined, propertyTypeNode(ctx, prop))));
|
|
101
|
+
if (model.indexer && model.indexer.key.name === "string") {
|
|
102
|
+
members.push(f.createIndexSignature(undefined, [f.createParameterDeclaration(undefined, undefined, "key", undefined, stringType())], typeToNode(ctx, model.indexer.value)));
|
|
103
|
+
}
|
|
104
|
+
return members;
|
|
105
|
+
}
|
|
106
|
+
function propertyTypeNode(ctx, prop) {
|
|
107
|
+
const override = getTsType(ctx.program, prop);
|
|
108
|
+
return override ? overrideNode(ctx, override) : typeToNode(ctx, prop.type);
|
|
109
|
+
}
|
|
110
|
+
function overrideNode(ctx, override) {
|
|
111
|
+
if (override.from) {
|
|
112
|
+
const root = override.type.split(/[.<]/)[0];
|
|
113
|
+
const names = ctx.imports.get(override.from) ?? new Set();
|
|
114
|
+
names.add(root);
|
|
115
|
+
ctx.imports.set(override.from, names);
|
|
116
|
+
}
|
|
117
|
+
return f.createTypeReferenceNode(override.type);
|
|
118
|
+
}
|
|
119
|
+
function propertyName(name) {
|
|
120
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? f.createIdentifier(name) : f.createStringLiteral(name);
|
|
121
|
+
}
|
|
122
|
+
function enumDeclaration(ctx, en) {
|
|
123
|
+
if (isLiteralUnion(ctx.program, en)) {
|
|
124
|
+
return withDoc(ctx, en, f.createTypeAliasDeclaration(exportModifier(), en.name, undefined, f.createUnionTypeNode([...en.members.values()].map((member) => literalNode(member.value ?? member.name)))));
|
|
125
|
+
}
|
|
126
|
+
const members = [...en.members.values()].map((member) => {
|
|
127
|
+
const value = member.value ?? member.name;
|
|
128
|
+
return withDoc(ctx, member, f.createEnumMember(propertyName(member.name), typeof value === "number" ? f.createNumericLiteral(value) : f.createStringLiteral(value)));
|
|
129
|
+
});
|
|
130
|
+
return withDoc(ctx, en, f.createEnumDeclaration(exportModifier(), en.name, members));
|
|
131
|
+
}
|
|
132
|
+
function literalNode(value) {
|
|
133
|
+
return f.createLiteralTypeNode(typeof value === "number" ? f.createNumericLiteral(value) : f.createStringLiteral(value));
|
|
134
|
+
}
|
|
135
|
+
function scalarDeclaration(ctx, scalar) {
|
|
136
|
+
const override = getTsType(ctx.program, scalar);
|
|
137
|
+
const base = scalar.baseScalar;
|
|
138
|
+
const node = override
|
|
139
|
+
? overrideNode(ctx, override)
|
|
140
|
+
: !base
|
|
141
|
+
? unknownType()
|
|
142
|
+
: inStdNamespace(base)
|
|
143
|
+
? stdScalarNode(ctx, base)
|
|
144
|
+
: f.createTypeReferenceNode(base.name);
|
|
145
|
+
return withDoc(ctx, scalar, f.createTypeAliasDeclaration(exportModifier(), scalar.name, undefined, node));
|
|
146
|
+
}
|
|
147
|
+
function returnTypeNode(ctx, op, container) {
|
|
148
|
+
const node = typeToNode(ctx, op.returnType);
|
|
149
|
+
const async = isAsync(ctx.program, op) || (container !== undefined && isAsync(ctx.program, container));
|
|
150
|
+
return async ? f.createTypeReferenceNode("Promise", [node]) : node;
|
|
151
|
+
}
|
|
152
|
+
function operationParameters(ctx, op) {
|
|
153
|
+
return [...op.parameters.properties.values()].map((prop) => f.createParameterDeclaration(undefined, undefined, prop.name, prop.optional ? f.createToken(ts.SyntaxKind.QuestionToken) : undefined, propertyTypeNode(ctx, prop)));
|
|
154
|
+
}
|
|
155
|
+
function interfaceDeclaration(ctx, iface) {
|
|
156
|
+
const members = [...iface.operations.values()].map((op) => withDoc(ctx, op, f.createMethodSignature(undefined, op.name, undefined, undefined, operationParameters(ctx, op), returnTypeNode(ctx, op, iface))));
|
|
157
|
+
return withDoc(ctx, iface, f.createInterfaceDeclaration(exportModifier(), iface.name, undefined, undefined, members));
|
|
158
|
+
}
|
|
159
|
+
function unionToNode(ctx, union) {
|
|
160
|
+
return f.createUnionTypeNode([...union.variants.values()].map((variant) => typeToNode(ctx, variant.type)));
|
|
161
|
+
}
|
|
162
|
+
function typeToNode(ctx, type) {
|
|
163
|
+
switch (type.kind) {
|
|
164
|
+
case "Model": {
|
|
165
|
+
// Inside template declarations Array<T>/Record<T> instances carry no
|
|
166
|
+
// indexer yet, so resolve them via their template argument instead.
|
|
167
|
+
const element = (type.name === "Array" || type.name === "Record") && inStdNamespace(type)
|
|
168
|
+
? (type.indexer?.value ?? typeArgument(type))
|
|
169
|
+
: undefined;
|
|
170
|
+
if (element) {
|
|
171
|
+
return type.name === "Array"
|
|
172
|
+
? f.createArrayTypeNode(typeToNode(ctx, element))
|
|
173
|
+
: f.createTypeReferenceNode("Record", [stringType(), typeToNode(ctx, element)]);
|
|
174
|
+
}
|
|
175
|
+
if (isArrayModelType(type)) {
|
|
176
|
+
return f.createArrayTypeNode(typeToNode(ctx, type.indexer.value));
|
|
177
|
+
}
|
|
178
|
+
if (isRecordModelType(type)) {
|
|
179
|
+
return f.createTypeReferenceNode("Record", [stringType(), typeToNode(ctx, type.indexer.value)]);
|
|
180
|
+
}
|
|
181
|
+
const ref = namedReference(ctx, type);
|
|
182
|
+
return ref
|
|
183
|
+
? f.createTypeReferenceNode(ref.name, ref.args)
|
|
184
|
+
: f.createTypeLiteralNode(modelMembers(ctx, type));
|
|
185
|
+
}
|
|
186
|
+
case "Scalar":
|
|
187
|
+
// User scalars are declared as aliases, so a @tsType override applies
|
|
188
|
+
// at the declaration site and references keep using the scalar's name.
|
|
189
|
+
return inStdNamespace(type) ? stdScalarNode(ctx, type) : f.createTypeReferenceNode(type.name);
|
|
190
|
+
case "Enum":
|
|
191
|
+
return f.createTypeReferenceNode(type.name);
|
|
192
|
+
case "EnumMember":
|
|
193
|
+
if (isLiteralUnion(ctx.program, type.enum)) {
|
|
194
|
+
return literalNode(type.value ?? type.name);
|
|
195
|
+
}
|
|
196
|
+
return f.createTypeReferenceNode(f.createQualifiedName(f.createIdentifier(type.enum.name), type.name));
|
|
197
|
+
case "Union": {
|
|
198
|
+
const ref = type.name ? namedReference(ctx, type) : undefined;
|
|
199
|
+
return ref ? f.createTypeReferenceNode(ref.name, ref.args) : unionToNode(ctx, type);
|
|
200
|
+
}
|
|
201
|
+
case "Tuple": {
|
|
202
|
+
const tuple = f.createTupleTypeNode(type.values.map((value) => typeToNode(ctx, value)));
|
|
203
|
+
return ts.setEmitFlags(tuple, ts.EmitFlags.SingleLine);
|
|
204
|
+
}
|
|
205
|
+
case "TemplateParameter":
|
|
206
|
+
return f.createTypeReferenceNode(type.node.id.sv);
|
|
207
|
+
case "String":
|
|
208
|
+
return f.createLiteralTypeNode(f.createStringLiteral(type.value));
|
|
209
|
+
case "Number":
|
|
210
|
+
return f.createLiteralTypeNode(f.createNumericLiteral(type.value));
|
|
211
|
+
case "Boolean":
|
|
212
|
+
return f.createLiteralTypeNode(type.value ? f.createTrue() : f.createFalse());
|
|
213
|
+
case "StringTemplate":
|
|
214
|
+
return stringTemplateToNode(ctx, type);
|
|
215
|
+
case "Intrinsic":
|
|
216
|
+
switch (type.name) {
|
|
217
|
+
case "null":
|
|
218
|
+
return f.createLiteralTypeNode(f.createNull());
|
|
219
|
+
case "void":
|
|
220
|
+
return f.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword);
|
|
221
|
+
case "never":
|
|
222
|
+
return f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword);
|
|
223
|
+
default:
|
|
224
|
+
return unknownType();
|
|
225
|
+
}
|
|
226
|
+
default:
|
|
227
|
+
return unknownType();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function stringTemplateToNode(ctx, template) {
|
|
231
|
+
if (template.stringValue !== undefined) {
|
|
232
|
+
return f.createLiteralTypeNode(f.createStringLiteral(template.stringValue));
|
|
233
|
+
}
|
|
234
|
+
const spans = [...template.spans];
|
|
235
|
+
let i = 0;
|
|
236
|
+
let head = "";
|
|
237
|
+
while (i < spans.length && !spans[i].isInterpolated) {
|
|
238
|
+
head += spans[i].type.value;
|
|
239
|
+
i++;
|
|
240
|
+
}
|
|
241
|
+
const tsSpans = [];
|
|
242
|
+
while (i < spans.length) {
|
|
243
|
+
const inner = typeToNode(ctx, spans[i].type);
|
|
244
|
+
i++;
|
|
245
|
+
let text = "";
|
|
246
|
+
while (i < spans.length && !spans[i].isInterpolated) {
|
|
247
|
+
text += spans[i].type.value;
|
|
248
|
+
i++;
|
|
249
|
+
}
|
|
250
|
+
tsSpans.push(f.createTemplateLiteralTypeSpan(inner, i >= spans.length ? f.createTemplateTail(text) : f.createTemplateMiddle(text)));
|
|
251
|
+
}
|
|
252
|
+
return f.createTemplateLiteralType(f.createTemplateHead(head), tsSpans);
|
|
253
|
+
}
|
|
254
|
+
function typeArgument(type) {
|
|
255
|
+
const arg = type.templateMapper?.args[0];
|
|
256
|
+
return arg?.entityKind === "Type" ? arg : undefined;
|
|
257
|
+
}
|
|
258
|
+
function namedReference(ctx, type) {
|
|
259
|
+
if (!type.name || inStdNamespace(type))
|
|
260
|
+
return undefined;
|
|
261
|
+
if (!type.templateMapper)
|
|
262
|
+
return { name: type.name };
|
|
263
|
+
if (!canEmitGeneric(ctx, type))
|
|
264
|
+
return undefined;
|
|
265
|
+
const args = [];
|
|
266
|
+
for (const arg of type.templateMapper.args) {
|
|
267
|
+
if (arg.entityKind !== "Type")
|
|
268
|
+
return undefined;
|
|
269
|
+
args.push(typeToNode(ctx, arg));
|
|
270
|
+
}
|
|
271
|
+
return { name: type.name, args };
|
|
272
|
+
}
|
|
273
|
+
function templateParamNodes(type) {
|
|
274
|
+
const node = type.node;
|
|
275
|
+
return node?.templateParameters ?? [];
|
|
276
|
+
}
|
|
277
|
+
// `valueof` constraints have no type-level equivalent; the checker resolves
|
|
278
|
+
// them to ErrorType, which marks the template as not emittable as a generic.
|
|
279
|
+
function resolveExpression(ctx, expr) {
|
|
280
|
+
const type = ctx.program.checker.getTypeForNode(expr);
|
|
281
|
+
return type.kind === "Intrinsic" && type.name === "ErrorType" ? undefined : type;
|
|
282
|
+
}
|
|
283
|
+
function canEmitGeneric(ctx, type) {
|
|
284
|
+
return templateParamNodes(type).every((param) => !param.constraint || resolveExpression(ctx, param.constraint));
|
|
285
|
+
}
|
|
286
|
+
function typeParameterDeclarations(ctx, type) {
|
|
287
|
+
const params = templateParamNodes(type);
|
|
288
|
+
if (params.length === 0)
|
|
289
|
+
return undefined;
|
|
290
|
+
return params.map((param) => {
|
|
291
|
+
const constraint = param.constraint && resolveExpression(ctx, param.constraint);
|
|
292
|
+
const defaultType = param.default && resolveExpression(ctx, param.default);
|
|
293
|
+
return f.createTypeParameterDeclaration(undefined, param.id.sv, constraint && typeToNode(ctx, constraint), defaultType && typeToNode(ctx, defaultType));
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function stdScalarNode(ctx, scalar) {
|
|
297
|
+
let current = scalar;
|
|
298
|
+
while (current) {
|
|
299
|
+
const override = getTsType(ctx.program, current);
|
|
300
|
+
if (override)
|
|
301
|
+
return overrideNode(ctx, override);
|
|
302
|
+
const mapped = scalarMap[current.name];
|
|
303
|
+
if (mapped)
|
|
304
|
+
return mapped();
|
|
305
|
+
current = current.baseScalar;
|
|
306
|
+
}
|
|
307
|
+
return unknownType();
|
|
308
|
+
}
|
|
309
|
+
function inStdNamespace(type) {
|
|
310
|
+
let ns = type.namespace;
|
|
311
|
+
while (ns?.namespace) {
|
|
312
|
+
if (!ns.namespace.name && !ns.namespace.namespace)
|
|
313
|
+
return ns.name === "TypeSpec";
|
|
314
|
+
ns = ns.namespace;
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
function withDoc(ctx, type, node) {
|
|
319
|
+
const doc = getDoc(ctx.program, type);
|
|
320
|
+
if (doc) {
|
|
321
|
+
ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, `* ${doc.replace(/\n/g, "\n * ")} `, true);
|
|
322
|
+
}
|
|
323
|
+
return node;
|
|
324
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { emitFile, resolvePath } from "@typespec/compiler";
|
|
2
|
+
import { emitTypes } from "./emitter.js";
|
|
3
|
+
export { $decorators } from "./decorators.js";
|
|
4
|
+
export { emitTypes } from "./emitter.js";
|
|
5
|
+
export { $lib } from "./lib.js";
|
|
6
|
+
export async function $onEmit(context) {
|
|
7
|
+
if (context.program.compilerOptions.noEmit)
|
|
8
|
+
return;
|
|
9
|
+
await emitFile(context.program, {
|
|
10
|
+
path: resolvePath(context.emitterOutputDir, "types.ts"),
|
|
11
|
+
content: emitTypes(context.program),
|
|
12
|
+
});
|
|
13
|
+
}
|
package/dist/lib.d.ts
ADDED
package/dist/lib.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createTypeSpecLibrary } from "@typespec/compiler";
|
|
2
|
+
export const $lib = createTypeSpecLibrary({
|
|
3
|
+
name: "just-the-type",
|
|
4
|
+
diagnostics: {},
|
|
5
|
+
state: {
|
|
6
|
+
async: { description: "Operations and interfaces whose return types are wrapped in Promise" },
|
|
7
|
+
readonly: { description: "Properties and models emitted with the readonly modifier" },
|
|
8
|
+
tsType: { description: "Raw TypeScript type overrides with optional import source" },
|
|
9
|
+
literalUnion: { description: "Enums emitted as unions of literals instead of TS enums" },
|
|
10
|
+
},
|
|
11
|
+
});
|
package/lib/main.tsp
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import "../dist/index.js";
|
|
2
|
+
|
|
3
|
+
using TypeSpec.Reflection;
|
|
4
|
+
|
|
5
|
+
namespace JustTheType;
|
|
6
|
+
|
|
7
|
+
/** Wrap the return type of the operation (or all operations of an interface) in Promise. */
|
|
8
|
+
extern dec promise(target: Operation | Interface);
|
|
9
|
+
|
|
10
|
+
/** Emit the property (or all properties of the model) with the readonly modifier. */
|
|
11
|
+
extern dec readonly(target: ModelProperty | Model);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Replace the emitted type with raw TypeScript.
|
|
15
|
+
* @param type TypeScript type text, e.g. "Date" or "Temporal.Instant".
|
|
16
|
+
* @param importFrom Optional module to type-import the type's root identifier from.
|
|
17
|
+
*/
|
|
18
|
+
extern dec tsType(target: Scalar | ModelProperty, type: valueof string, importFrom?: valueof string);
|
|
19
|
+
|
|
20
|
+
/** Emit the enum as a union of its literal values instead of a TS enum. */
|
|
21
|
+
extern dec literalUnion(target: Enum);
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "just-the-type",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeSpec emitter that emits plain TypeScript types",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"typespec",
|
|
7
|
+
"typespec-emitter",
|
|
8
|
+
"typescript",
|
|
9
|
+
"codegen",
|
|
10
|
+
"types"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Daniel Kneip",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/daniel-kneip/just-the-type.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/daniel-kneip/just-the-type#readme",
|
|
19
|
+
"bugs": "https://github.com/daniel-kneip/just-the-type/issues",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"typespec": "./lib/main.tsp",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"lib"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@typespec/compiler": ">=1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"typescript": "^5.9.3"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@typespec/compiler": "1.13.0",
|
|
44
|
+
"vitest": "4.1.8"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"sample": "tsp compile sample"
|
|
50
|
+
}
|
|
51
|
+
}
|