+164
-1
DOCS.md
+164
-1
DOCS.md
···
312
312
313
313
Note that `Caption` won't exist as a separate def—the abstraction is erased in the output.
314
314
315
+
### Scalars
316
+
317
+
TypeSpec scalars let you create named types with constraints. **By default, scalars create standalone defs** (like models):
318
+
319
+
```typescript
320
+
import "@typelex/emitter";
321
+
322
+
namespace com.example {
323
+
model Main {
324
+
handle?: Handle;
325
+
bio?: Bio;
326
+
}
327
+
328
+
@maxLength(50)
329
+
scalar Handle extends string;
330
+
331
+
@maxLength(256)
332
+
@maxGraphemes(128)
333
+
scalar Bio extends string;
334
+
}
335
+
```
336
+
337
+
This creates three defs: `main`, `handle`, and `bio`:
338
+
339
+
```json
340
+
{
341
+
"id": "com.example",
342
+
"defs": {
343
+
"main": {
344
+
"type": "object",
345
+
"properties": {
346
+
"handle": { "type": "ref", "ref": "#handle" },
347
+
"bio": { "type": "ref", "ref": "#bio" }
348
+
}
349
+
},
350
+
"handle": {
351
+
"type": "string",
352
+
"maxLength": 50
353
+
},
354
+
"bio": {
355
+
"type": "string",
356
+
"maxLength": 256,
357
+
"maxGraphemes": 128
358
+
}
359
+
}
360
+
}
361
+
```
362
+
363
+
Use `@inline` to expand a scalar inline instead:
364
+
365
+
```typescript
366
+
import "@typelex/emitter";
367
+
368
+
namespace com.example {
369
+
model Main {
370
+
handle?: Handle;
371
+
}
372
+
373
+
@inline
374
+
@maxLength(50)
375
+
scalar Handle extends string;
376
+
}
377
+
```
378
+
379
+
Now `Handle` is expanded inline (no separate def):
380
+
381
+
```json
382
+
// ...
383
+
"properties": {
384
+
"handle": { "type": "string", "maxLength": 50 }
385
+
}
386
+
// ...
387
+
```
388
+
315
389
## Top-Level Lexicon Types
316
390
317
391
TypeSpec uses `model` for almost everything. Decorators specify what kind of Lexicon type it becomes.
···
905
979
906
980
## Defaults and Constants
907
981
908
-
### Defaults
982
+
### Property Defaults
983
+
984
+
You can set default values on properties:
909
985
910
986
```typescript
911
987
import "@typelex/emitter";
···
919
995
```
920
996
921
997
Maps to: `{"default": 1}`, `{"default": "en"}`
998
+
999
+
### Type Defaults
1000
+
1001
+
You can also set defaults on scalar and union types using the `@default` decorator:
1002
+
1003
+
```typescript
1004
+
import "@typelex/emitter";
1005
+
1006
+
namespace com.example {
1007
+
model Main {
1008
+
mode?: Mode;
1009
+
priority?: Priority;
1010
+
}
1011
+
1012
+
@default("standard")
1013
+
scalar Mode extends string;
1014
+
1015
+
@default(1)
1016
+
@closed
1017
+
@inline
1018
+
union Priority { 1, 2, 3 }
1019
+
}
1020
+
```
1021
+
1022
+
This creates a default on the type definition itself:
1023
+
1024
+
```json
1025
+
{
1026
+
"defs": {
1027
+
"mode": {
1028
+
"type": "string",
1029
+
"default": "standard"
1030
+
}
1031
+
}
1032
+
}
1033
+
```
1034
+
1035
+
For unions with token references, pass the model directly:
1036
+
1037
+
```typescript
1038
+
import "@typelex/emitter";
1039
+
1040
+
namespace com.example {
1041
+
model Main {
1042
+
eventType?: EventType;
1043
+
}
1044
+
1045
+
@default(InPerson)
1046
+
union EventType { Hybrid, InPerson, Virtual, string }
1047
+
1048
+
@token model Hybrid {}
1049
+
@token model InPerson {}
1050
+
@token model Virtual {}
1051
+
}
1052
+
```
1053
+
1054
+
This resolves to the fully-qualified token NSID:
1055
+
1056
+
```json
1057
+
{
1058
+
"eventType": {
1059
+
"type": "string",
1060
+
"knownValues": [
1061
+
"com.example#hybrid",
1062
+
"com.example#inPerson",
1063
+
"com.example#virtual"
1064
+
],
1065
+
"default": "com.example#inPerson"
1066
+
}
1067
+
}
1068
+
```
1069
+
1070
+
**Important:** When a scalar or union creates a standalone def (not `@inline`), property-level defaults must match the type's `@default`. Otherwise you'll get an error:
1071
+
1072
+
```typescript
1073
+
@default("standard")
1074
+
scalar Mode extends string;
1075
+
1076
+
model Main {
1077
+
mode?: Mode = "custom"; // ERROR: Conflicting defaults!
1078
+
}
1079
+
```
1080
+
1081
+
Solutions:
1082
+
1. Make the defaults match: `mode?: Mode = "standard"`
1083
+
2. Mark the type `@inline`: Allows property-level defaults
1084
+
3. Remove the property default: Uses the type's default
922
1085
923
1086
### Constants
924
1087
+40
packages/emitter/lib/decorators.tsp
+40
packages/emitter/lib/decorators.tsp
···
163
163
extern dec errors(target: unknown, ...errors: unknown[]);
164
164
165
165
/**
166
+
* Forces a model, scalar, or union to be inlined instead of creating a standalone def.
167
+
* By default, named types create separate definitions with references.
168
+
* Use @inline to expand the type inline at each usage site.
169
+
*
170
+
* @example Inline model
171
+
* ```typespec
172
+
* @inline
173
+
* model Caption {
174
+
* text?: string;
175
+
* }
176
+
*
177
+
* model Main {
178
+
* captions?: Caption[]; // Expands inline, no separate "caption" def
179
+
* }
180
+
* ```
181
+
*
182
+
* @example Inline scalar
183
+
* ```typespec
184
+
* @inline
185
+
* @maxLength(50)
186
+
* scalar Handle extends string;
187
+
*
188
+
* model Main {
189
+
* handle?: Handle; // Expands to { type: "string", maxLength: 50 }
190
+
* }
191
+
* ```
192
+
*
193
+
* @example Inline union
194
+
* ```typespec
195
+
* @inline
196
+
* union Status { "active", "inactive", string }
197
+
*
198
+
* model Main {
199
+
* status?: Status; // Expands inline with knownValues
200
+
* }
201
+
* ```
202
+
*/
203
+
extern dec inline(target: unknown);
204
+
205
+
/**
166
206
* Specifies a default value for a scalar or union definition.
167
207
* Only valid on standalone scalar or union defs (not @inline).
168
208
* The value must match the underlying type (string, integer, or boolean).
+39
-6
packages/emitter/src/emitter.ts
+39
-6
packages/emitter/src/emitter.ts
···
641
641
isClosed(this.program, unionType)
642
642
) {
643
643
const propDesc = prop ? getDoc(this.program, prop) : undefined;
644
-
const defaultValue = prop?.defaultValue
645
-
? serializeValueAsJson(this.program, prop.defaultValue, prop)
646
-
: undefined;
644
+
645
+
// Check for default value: property default takes precedence, then union's @default
646
+
let defaultValue: string | number | boolean | undefined;
647
+
if (prop?.defaultValue !== undefined) {
648
+
defaultValue = serializeValueAsJson(this.program, prop.defaultValue, prop) as any;
649
+
} else {
650
+
// If no property default, check union's @default decorator
651
+
const rawUnionDefault = getDefault(this.program, unionType);
652
+
const unionDefault = this.processDefaultValue(rawUnionDefault);
653
+
if (unionDefault !== undefined && typeof unionDefault === 'number') {
654
+
defaultValue = unionDefault;
655
+
}
656
+
}
657
+
647
658
return {
648
659
type: "integer",
649
660
enum: variants.numericLiterals,
···
666
677
) {
667
678
const isClosedUnion = isClosed(this.program, unionType);
668
679
const propDesc = prop ? getDoc(this.program, prop) : undefined;
669
-
const defaultValue = prop?.defaultValue
670
-
? serializeValueAsJson(this.program, prop.defaultValue, prop)
671
-
: undefined;
680
+
681
+
// Check for default value: property default takes precedence, then union's @default
682
+
let defaultValue: string | number | boolean | undefined;
683
+
if (prop?.defaultValue !== undefined) {
684
+
defaultValue = serializeValueAsJson(this.program, prop.defaultValue, prop) as any;
685
+
} else {
686
+
// If no property default, check union's @default decorator
687
+
const rawUnionDefault = getDefault(this.program, unionType);
688
+
const unionDefault = this.processDefaultValue(rawUnionDefault);
689
+
690
+
if (unionDefault !== undefined) {
691
+
// Check if it's a Type (model reference for tokens)
692
+
if (typeof unionDefault === 'object' && 'kind' in unionDefault && unionDefault.kind === 'Model') {
693
+
// Resolve the model reference to its NSID
694
+
const tokenModel = unionDefault as Model;
695
+
const tokenRef = this.getModelReference(tokenModel, true); // fullyQualified=true
696
+
if (tokenRef) {
697
+
defaultValue = tokenRef;
698
+
}
699
+
} else if (typeof unionDefault === 'string') {
700
+
defaultValue = unionDefault;
701
+
}
702
+
}
703
+
}
704
+
672
705
const maxLength = getMaxLength(this.program, unionType);
673
706
const minLength = getMinLength(this.program, unionType);
674
707
const maxGraphemes = getMaxGraphemes(this.program, unionType);
+125
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/calendar/event.tsp
+125
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/calendar/event.tsp
···
1
+
import "@typelex/emitter";
2
+
3
+
namespace community.lexicon.calendar.event {
4
+
/** A calendar event. */
5
+
@rec("tid")
6
+
model Main {
7
+
/** The name of the event. */
8
+
@required
9
+
name: string;
10
+
11
+
/** The description of the event. */
12
+
description?: string;
13
+
14
+
/** Client-declared timestamp when the event was created. */
15
+
@required
16
+
createdAt: datetime;
17
+
18
+
/** Client-declared timestamp when the event starts. */
19
+
startsAt?: datetime;
20
+
21
+
/** Client-declared timestamp when the event ends. */
22
+
endsAt?: datetime;
23
+
24
+
/** The attendance mode of the event. */
25
+
mode?: Mode;
26
+
27
+
/** The status of the event. */
28
+
status?: Status;
29
+
30
+
/** The locations where the event takes place. */
31
+
locations?: (
32
+
| Uri
33
+
| community.lexicon.location.address.Main
34
+
| community.lexicon.location.fsq.Main
35
+
| community.lexicon.location.geo.Main
36
+
| community.lexicon.location.hthree.Main
37
+
)[];
38
+
39
+
/** URIs associated with the event. */
40
+
uris?: Uri[];
41
+
}
42
+
43
+
/** The mode of the event. */
44
+
@default(Inperson)
45
+
union Mode {
46
+
Hybrid,
47
+
Inperson,
48
+
Virtual,
49
+
string,
50
+
}
51
+
52
+
/** A virtual event that takes place online. */
53
+
@token
54
+
model Virtual {}
55
+
56
+
/** An in-person event that takes place offline. */
57
+
@token
58
+
model Inperson {}
59
+
60
+
/** A hybrid event that takes place both online and offline. */
61
+
@token
62
+
model Hybrid {}
63
+
64
+
/** The status of the event. */
65
+
@default(Scheduled)
66
+
union Status {
67
+
Cancelled,
68
+
Planned,
69
+
Postponed,
70
+
Rescheduled,
71
+
Scheduled,
72
+
string,
73
+
}
74
+
75
+
/** The event has been created, but not finalized. */
76
+
@token
77
+
model Planned {}
78
+
79
+
/** The event has been created and scheduled. */
80
+
@token
81
+
model Scheduled {}
82
+
83
+
/** The event has been rescheduled. */
84
+
@token
85
+
model Rescheduled {}
86
+
87
+
/** The event has been cancelled. */
88
+
@token
89
+
model Cancelled {}
90
+
91
+
/** The event has been postponed and a new start date has not been set. */
92
+
@token
93
+
model Postponed {}
94
+
95
+
/** A URI associated with the event. */
96
+
model Uri {
97
+
@required
98
+
uri: uri;
99
+
100
+
/** The display name of the URI. */
101
+
name?: string;
102
+
}
103
+
}
104
+
105
+
// --- Externals ---
106
+
107
+
@external
108
+
namespace community.lexicon.location.address {
109
+
model Main {}
110
+
}
111
+
112
+
@external
113
+
namespace community.lexicon.location.fsq {
114
+
model Main {}
115
+
}
116
+
117
+
@external
118
+
namespace community.lexicon.location.geo {
119
+
model Main {}
120
+
}
121
+
122
+
@external
123
+
namespace community.lexicon.location.hthree {
124
+
model Main {}
125
+
}
+41
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/calendar/rsvp.tsp
+41
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/calendar/rsvp.tsp
···
1
+
import "@typelex/emitter";
2
+
3
+
namespace community.lexicon.calendar.rsvp {
4
+
/** An RSVP for an event. */
5
+
@rec("tid")
6
+
model Main {
7
+
@required
8
+
subject: `com`.atproto.repo.strongRef.Main;
9
+
10
+
@required
11
+
status: Status;
12
+
}
13
+
14
+
@inline
15
+
@default(Going)
16
+
union Status {
17
+
Interested,
18
+
Going,
19
+
Notgoing,
20
+
string,
21
+
}
22
+
23
+
/** Interested in the event */
24
+
@token
25
+
model Interested {}
26
+
27
+
/** Going to the event */
28
+
@token
29
+
model Going {}
30
+
31
+
/** Not going to the event */
32
+
@token
33
+
model Notgoing {}
34
+
}
35
+
36
+
// --- Externals ---
37
+
38
+
@external
39
+
namespace `com`.atproto.repo.strongRef {
40
+
model Main {}
41
+
}
+119
-1
packages/emitter/test/integration/lexicon-examples/output/community/lexicon/calendar/event.json
+119
-1
packages/emitter/test/integration/lexicon-examples/output/community/lexicon/calendar/event.json
···
2
2
"lexicon": 1,
3
3
"id": "community.lexicon.calendar.event",
4
4
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "A calendar event.",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": [
12
+
"name",
13
+
"createdAt"
14
+
],
15
+
"properties": {
16
+
"name": {
17
+
"type": "string",
18
+
"description": "The name of the event."
19
+
},
20
+
"description": {
21
+
"type": "string",
22
+
"description": "The description of the event."
23
+
},
24
+
"createdAt": {
25
+
"type": "string",
26
+
"format": "datetime",
27
+
"description": "Client-declared timestamp when the event was created."
28
+
},
29
+
"startsAt": {
30
+
"type": "string",
31
+
"format": "datetime",
32
+
"description": "Client-declared timestamp when the event starts."
33
+
},
34
+
"endsAt": {
35
+
"type": "string",
36
+
"format": "datetime",
37
+
"description": "Client-declared timestamp when the event ends."
38
+
},
39
+
"mode": {
40
+
"type": "ref",
41
+
"ref": "#mode",
42
+
"description": "The attendance mode of the event."
43
+
},
44
+
"status": {
45
+
"type": "ref",
46
+
"ref": "#status",
47
+
"description": "The status of the event."
48
+
},
49
+
"locations": {
50
+
"type": "array",
51
+
"description": "The locations where the event takes place.",
52
+
"items": {
53
+
"type": "union",
54
+
"refs": [
55
+
"#uri",
56
+
"community.lexicon.location.address",
57
+
"community.lexicon.location.fsq",
58
+
"community.lexicon.location.geo",
59
+
"community.lexicon.location.hthree"
60
+
]
61
+
}
62
+
},
63
+
"uris": {
64
+
"type": "array",
65
+
"description": "URIs associated with the event.",
66
+
"items": {
67
+
"type": "ref",
68
+
"ref": "#uri"
69
+
}
70
+
}
71
+
}
72
+
}
73
+
},
5
74
"mode": {
6
75
"type": "string",
7
76
"description": "The mode of the event.",
···
23
92
"hybrid": {
24
93
"type": "token",
25
94
"description": "A hybrid event that takes place both online and offline."
95
+
},
96
+
"status": {
97
+
"type": "string",
98
+
"description": "The status of the event.",
99
+
"default": "community.lexicon.calendar.event#scheduled",
100
+
"knownValues": [
101
+
"community.lexicon.calendar.event#cancelled",
102
+
"community.lexicon.calendar.event#planned",
103
+
"community.lexicon.calendar.event#postponed",
104
+
"community.lexicon.calendar.event#rescheduled",
105
+
"community.lexicon.calendar.event#scheduled"
106
+
]
107
+
},
108
+
"planned": {
109
+
"type": "token",
110
+
"description": "The event has been created, but not finalized."
111
+
},
112
+
"scheduled": {
113
+
"type": "token",
114
+
"description": "The event has been created and scheduled."
115
+
},
116
+
"rescheduled": {
117
+
"type": "token",
118
+
"description": "The event has been rescheduled."
119
+
},
120
+
"cancelled": {
121
+
"type": "token",
122
+
"description": "The event has been cancelled."
123
+
},
124
+
"postponed": {
125
+
"type": "token",
126
+
"description": "The event has been postponed and a new start date has not been set."
127
+
},
128
+
"uri": {
129
+
"type": "object",
130
+
"description": "A URI associated with the event.",
131
+
"required": [
132
+
"uri"
133
+
],
134
+
"properties": {
135
+
"uri": {
136
+
"type": "string",
137
+
"format": "uri"
138
+
},
139
+
"name": {
140
+
"type": "string",
141
+
"description": "The display name of the URI."
142
+
}
143
+
}
26
144
}
27
145
}
28
-
}
146
+
}