···163163extern dec errors(target: unknown, ...errors: unknown[]);
164164165165/**
166166+ * Specifies a default value for a scalar or union definition.
167167+ * Only valid on standalone scalar or union defs (not @inline).
168168+ * The value must match the underlying type (string, integer, or boolean).
169169+ * For unions with token refs, you can pass a model reference directly.
170170+ *
171171+ * @param value - The default value (literal or model reference for tokens)
172172+ *
173173+ * @example Scalar with default
174174+ * ```typespec
175175+ * @default("standard")
176176+ * scalar Mode extends string;
177177+ * ```
178178+ *
179179+ * @example Union with token default
180180+ * ```typespec
181181+ * @default(Inperson)
182182+ * union EventMode { Hybrid, Inperson, Virtual, string }
183183+ *
184184+ * @token
185185+ * model Inperson {}
186186+ * ```
187187+ */
188188+extern dec `default`(target: unknown, value: unknown);
189189+190190+/**
166191 * Marks a namespace as external, preventing it from emitting JSON output.
167192 * This decorator can only be applied to namespaces.
168193 * Useful for importing definitions from other lexicons without re-emitting them.
+17
packages/emitter/src/decorators.ts
···2525const maxBytesKey = Symbol("maxBytes");
2626const minBytesKey = Symbol("minBytes");
2727const externalKey = Symbol("external");
2828+const defaultKey = Symbol("default");
28292930/**
3031 * @maxBytes decorator for maximum length of bytes type
···294295295296export function isReadOnly(program: Program, target: Type): boolean {
296297 return program.stateSet(readOnlyKey).has(target);
298298+}
299299+300300+/**
301301+ * @default decorator for setting default values on scalars and unions
302302+ * The value can be a literal (string, number, boolean) or a model reference for tokens
303303+ */
304304+export function $default(context: DecoratorContext, target: Type, value: any) {
305305+ // Just store the raw value - let the emitter handle unwrapping and validation
306306+ context.program.stateMap(defaultKey).set(target, value);
307307+}
308308+309309+export function getDefault(
310310+ program: Program,
311311+ target: Type,
312312+): any | undefined {
313313+ return program.stateMap(defaultKey).get(target);
297314}
298315299316/**
+235-16
packages/emitter/src/emitter.ts
···6868 getMaxBytes,
6969 getMinBytes,
7070 isExternal,
7171+ getDefault,
7172} from "./decorators.js";
72737374export interface EmitterOptions {
···9798 private program: Program,
9899 private options: EmitterOptions,
99100 ) {}
101101+102102+ /**
103103+ * Process the raw default value from the decorator, unwrapping TypeSpec value objects
104104+ * and returning either a primitive (string, number, boolean) or a Type (for model references)
105105+ */
106106+ private processDefaultValue(rawValue: any): string | number | boolean | Type | undefined {
107107+ if (rawValue === undefined) return undefined;
108108+109109+ // TypeSpec may wrap values - check if this is a value object first
110110+ if (rawValue && typeof rawValue === 'object' && rawValue.valueKind) {
111111+ if (rawValue.valueKind === "StringValue") {
112112+ return rawValue.value;
113113+ } else if (rawValue.valueKind === "NumericValue" || rawValue.valueKind === "NumberValue") {
114114+ return rawValue.value;
115115+ } else if (rawValue.valueKind === "BooleanValue") {
116116+ return rawValue.value;
117117+ }
118118+ return undefined; // Unsupported valueKind
119119+ }
120120+121121+ // Check if it's a Type object (Model, String, Number, Boolean literals)
122122+ if (rawValue && typeof rawValue === 'object' && rawValue.kind) {
123123+ if (rawValue.kind === "String") {
124124+ return (rawValue as StringLiteral).value;
125125+ } else if (rawValue.kind === "Number") {
126126+ return (rawValue as NumericLiteral).value;
127127+ } else if (rawValue.kind === "Boolean") {
128128+ return (rawValue as BooleanLiteral).value;
129129+ } else if (rawValue.kind === "Model") {
130130+ // Return the model itself for token references
131131+ return rawValue as Model;
132132+ }
133133+ return undefined; // Unsupported kind
134134+ }
135135+136136+ // Direct primitive value
137137+ if (typeof rawValue === 'string' || typeof rawValue === 'number' || typeof rawValue === 'boolean') {
138138+ return rawValue;
139139+ }
140140+141141+ return undefined;
142142+ }
100143101144 async emit() {
102145 const globalNs = this.program.getGlobalNamespaceType();
···356399 }
357400358401 private addScalarToDefs(lexicon: LexiconDoc, scalar: Scalar) {
402402+ // Only skip if the scalar itself is in TypeSpec namespace (built-in scalars)
359403 if (scalar.namespace?.name === "TypeSpec") return;
360360- if (scalar.baseScalar?.namespace?.name === "TypeSpec") return;
361404362405 // Skip @inline scalars - they should be inlined, not defined separately
363406 if (isInline(this.program, scalar)) {
···368411 const scalarDef = this.scalarToLexiconPrimitive(scalar, undefined);
369412 if (scalarDef) {
370413 const description = getDoc(this.program, scalar);
371371- lexicon.defs[defName] = { ...scalarDef, description } as LexUserType;
414414+415415+ // Apply @default decorator if present
416416+ const rawDefault = getDefault(this.program, scalar);
417417+ const defaultValue = this.processDefaultValue(rawDefault);
418418+ let defWithDefault: any = { ...scalarDef };
419419+420420+ if (defaultValue !== undefined) {
421421+ // Check if it's a Type (model reference for tokens)
422422+ if (typeof defaultValue === 'object' && 'kind' in defaultValue) {
423423+ // For model references, we need to resolve to NSID
424424+ // This shouldn't happen for scalars, only unions support token refs
425425+ this.program.reportDiagnostic({
426426+ code: "invalid-default-on-scalar",
427427+ severity: "error",
428428+ message: "@default on scalars must be a literal value (string, number, or boolean), not a model reference",
429429+ target: scalar,
430430+ });
431431+ } else {
432432+ // Validate that the default value matches the type
433433+ this.assertValidValueForType(scalarDef.type, defaultValue, scalar);
434434+ defWithDefault = { ...defWithDefault, default: defaultValue };
435435+ }
436436+ }
437437+438438+ // Apply integer constraints for standalone scalar defs
439439+ if (scalarDef.type === "integer") {
440440+ const minValue = getMinValue(this.program, scalar);
441441+ if (minValue !== undefined) {
442442+ (defWithDefault as any).minimum = minValue;
443443+ }
444444+ const maxValue = getMaxValue(this.program, scalar);
445445+ if (maxValue !== undefined) {
446446+ (defWithDefault as any).maximum = maxValue;
447447+ }
448448+ }
449449+450450+ lexicon.defs[defName] = { ...defWithDefault, description } as LexUserType;
372451 }
373452 }
374453···391470 if (unionDef.type === "string" && (unionDef.knownValues || unionDef.enum)) {
392471 const defName = name.charAt(0).toLowerCase() + name.slice(1);
393472 const description = getDoc(this.program, union);
394394- lexicon.defs[defName] = { ...unionDef, description };
473473+474474+ // Apply @default decorator if present
475475+ const rawDefault = getDefault(this.program, union);
476476+ const defaultValue = this.processDefaultValue(rawDefault);
477477+ let defWithDefault: any = { ...unionDef };
478478+479479+ if (defaultValue !== undefined) {
480480+ // Check if it's a Type (model reference for tokens)
481481+ if (typeof defaultValue === 'object' && 'kind' in defaultValue) {
482482+ // Resolve the model reference to its NSID
483483+ const tokenModel = defaultValue as Model;
484484+ const tokenRef = this.getModelReference(tokenModel, true); // fullyQualified=true
485485+ if (tokenRef) {
486486+ defWithDefault = { ...defWithDefault, default: tokenRef };
487487+ } else {
488488+ this.program.reportDiagnostic({
489489+ code: "invalid-default-token",
490490+ severity: "error",
491491+ message: "@default value must be a valid token model reference",
492492+ target: union,
493493+ });
494494+ }
495495+ } else {
496496+ // Literal value - validate it matches the union type
497497+ if (typeof defaultValue !== "string") {
498498+ this.program.reportDiagnostic({
499499+ code: "invalid-default-value-type",
500500+ severity: "error",
501501+ message: `Default value type mismatch: expected string, got ${typeof defaultValue}`,
502502+ target: union,
503503+ });
504504+ } else {
505505+ defWithDefault = { ...defWithDefault, default: defaultValue };
506506+ }
507507+ }
508508+ }
509509+510510+ lexicon.defs[defName] = { ...defWithDefault, description };
395511 } else if (unionDef.type === "union") {
396512 this.program.reportDiagnostic({
397513 code: "union-refs-not-allowed-as-def",
···401517 `Use @inline to inline them at usage sites, use @token models for known values, or use string literals.`,
402518 target: union,
403519 });
520520+ } else if (unionDef.type === "integer" && (unionDef as any).enum) {
521521+ // Integer enums can also be defs
522522+ const defName = name.charAt(0).toLowerCase() + name.slice(1);
523523+ const description = getDoc(this.program, union);
524524+525525+ // Apply @default decorator if present
526526+ const rawDefault = getDefault(this.program, union);
527527+ const defaultValue = this.processDefaultValue(rawDefault);
528528+ let defWithDefault = { ...unionDef };
529529+530530+ if (defaultValue !== undefined) {
531531+ if (typeof defaultValue === "number") {
532532+ defWithDefault = { ...defWithDefault, default: defaultValue };
533533+ } else {
534534+ this.program.reportDiagnostic({
535535+ code: "invalid-default-value-type",
536536+ severity: "error",
537537+ message: `Default value type mismatch: expected integer, got ${typeof defaultValue}`,
538538+ target: union,
539539+ });
540540+ }
541541+ }
542542+543543+ lexicon.defs[defName] = { ...defWithDefault, description };
404544 }
405545 }
406546···11581298 prop?: ModelProperty,
11591299 propDesc?: string,
11601300 ): LexObjectProperty | null {
13011301+ // Check if this scalar should be referenced instead of inlined
13021302+ const scalarRef = this.getScalarReference(scalar);
13031303+ if (scalarRef) {
13041304+ // Check if property has a default value that would conflict with the scalar's @default
13051305+ if (prop?.defaultValue !== undefined) {
13061306+ const scalarDefaultRaw = getDefault(this.program, scalar);
13071307+ const scalarDefault = this.processDefaultValue(scalarDefaultRaw);
13081308+ const propDefault = serializeValueAsJson(this.program, prop.defaultValue, prop);
13091309+13101310+ // If the scalar has a different default, or if the property has a default but the scalar doesn't, error
13111311+ if (scalarDefault !== propDefault) {
13121312+ this.program.reportDiagnostic({
13131313+ code: "conflicting-defaults",
13141314+ severity: "error",
13151315+ message: scalarDefault !== undefined
13161316+ ? `Property default value conflicts with scalar's @default decorator. The scalar "${scalar.name}" has @default(${JSON.stringify(scalarDefault)}) but property has default value ${JSON.stringify(propDefault)}. Either remove the property default, mark the scalar @inline, or make the defaults match.`
13171317+ : `Property has a default value but the referenced scalar "${scalar.name}" does not. Either add @default to the scalar, mark it @inline to allow property-level defaults, or remove the property default.`,
13181318+ target: prop,
13191319+ });
13201320+ }
13211321+ }
13221322+13231323+ return { type: "ref" as const, ref: scalarRef, description: propDesc };
13241324+ }
13251325+13261326+ // Inline the scalar
11611327 const primitive = this.scalarToLexiconPrimitive(scalar, prop);
11621328 if (!primitive) return null;
11631329···12461412 if (!isDefining) {
12471413 const unionRef = this.getUnionReference(unionType);
12481414 if (unionRef) {
14151415+ // Check if property has a default value that would conflict with the union's @default
14161416+ if (prop?.defaultValue !== undefined) {
14171417+ const unionDefaultRaw = getDefault(this.program, unionType);
14181418+ const unionDefault = this.processDefaultValue(unionDefaultRaw);
14191419+ const propDefault = serializeValueAsJson(this.program, prop.defaultValue, prop);
14201420+14211421+ // For union defaults that are model references, we need to resolve them for comparison
14221422+ let resolvedUnionDefault: string | number | boolean | undefined = unionDefault as any;
14231423+ if (unionDefault && typeof unionDefault === 'object' && 'kind' in unionDefault && unionDefault.kind === 'Model') {
14241424+ const ref = this.getModelReference(unionDefault as Model, true);
14251425+ resolvedUnionDefault = ref || undefined;
14261426+ }
14271427+14281428+ // If the union has a different default, or if the property has a default but the union doesn't, error
14291429+ if (resolvedUnionDefault !== propDefault) {
14301430+ this.program.reportDiagnostic({
14311431+ code: "conflicting-defaults",
14321432+ severity: "error",
14331433+ message: unionDefault !== undefined
14341434+ ? `Property default value conflicts with union's @default decorator. The union "${unionType.name}" has @default(${JSON.stringify(resolvedUnionDefault)}) but property has default value ${JSON.stringify(propDefault)}. Either remove the property default, mark the union @inline, or make the defaults match.`
14351435+ : `Property has a default value but the referenced union "${unionType.name}" does not. Either add @default to the union, mark it @inline to allow property-level defaults, or remove the property default.`,
14361436+ target: prop,
14371437+ });
14381438+ }
14391439+ }
14401440+12491441 return { type: "ref" as const, ref: unionRef, description: propDesc };
12501442 }
12511443 }
···12711463 // Check if this scalar (or its base) is bytes type
12721464 if (this.isScalarOfType(scalar, "bytes")) {
12731465 const byteDef: LexBytes = { type: "bytes" };
12741274- const target = prop || scalar;
1275146612761276- const minLength = getMinBytes(this.program, target);
14671467+ // Check scalar first for its own constraints, then property overrides
14681468+ const minLength = getMinBytes(this.program, scalar) ?? (prop ? getMinBytes(this.program, prop) : undefined);
12771469 if (minLength !== undefined) {
12781470 byteDef.minLength = minLength;
12791471 }
1280147212811281- const maxLength = getMaxBytes(this.program, target);
14731473+ const maxLength = getMaxBytes(this.program, scalar) ?? (prop ? getMaxBytes(this.program, prop) : undefined);
12821474 if (maxLength !== undefined) {
12831475 byteDef.maxLength = maxLength;
12841476 }
···1310150213111503 // Apply string constraints
13121504 if (primitive.type === "string") {
13131313- const target = prop || scalar;
13141314- const maxLength = getMaxLength(this.program, target);
15051505+ // Check scalar first for its own constraints, then property overrides
15061506+ const maxLength = getMaxLength(this.program, scalar) ?? (prop ? getMaxLength(this.program, prop) : undefined);
13151507 if (maxLength !== undefined) {
13161508 primitive.maxLength = maxLength;
13171509 }
13181318- const minLength = getMinLength(this.program, target);
15101510+ const minLength = getMinLength(this.program, scalar) ?? (prop ? getMinLength(this.program, prop) : undefined);
13191511 if (minLength !== undefined) {
13201512 primitive.minLength = minLength;
13211513 }
13221322- const maxGraphemes = getMaxGraphemes(this.program, target);
15141514+ const maxGraphemes = getMaxGraphemes(this.program, scalar) ?? (prop ? getMaxGraphemes(this.program, prop) : undefined);
13231515 if (maxGraphemes !== undefined) {
13241516 primitive.maxGraphemes = maxGraphemes;
13251517 }
13261326- const minGraphemes = getMinGraphemes(this.program, target);
15181518+ const minGraphemes = getMinGraphemes(this.program, scalar) ?? (prop ? getMinGraphemes(this.program, prop) : undefined);
13271519 if (minGraphemes !== undefined) {
13281520 primitive.minGraphemes = minGraphemes;
13291521 }
13301522 }
1331152313321524 // Apply numeric constraints
13331333- if (prop && primitive.type === "integer") {
13341334- const minValue = getMinValue(this.program, prop);
15251525+ if (primitive.type === "integer") {
15261526+ // Check scalar first for its own constraints, then property overrides
15271527+ const minValue = getMinValue(this.program, scalar) ?? (prop ? getMinValue(this.program, prop) : undefined);
13351528 if (minValue !== undefined) {
13361529 primitive.minimum = minValue;
13371530 }
13381338- const maxValue = getMaxValue(this.program, prop);
15311531+ const maxValue = getMaxValue(this.program, scalar) ?? (prop ? getMaxValue(this.program, prop) : undefined);
13391532 if (maxValue !== undefined) {
13401533 primitive.maximum = maxValue;
13411534 }
···14311624 private assertValidValueForType(
14321625 primitiveType: string,
14331626 value: unknown,
14341434- prop: ModelProperty,
16271627+ target: ModelProperty | Scalar | Union,
14351628 ): void {
14361629 const valid =
14371630 (primitiveType === "boolean" && typeof value === "boolean") ||
···14421635 code: "invalid-default-value-type",
14431636 severity: "error",
14441637 message: `Default value type mismatch: expected ${primitiveType}, got ${typeof value}`,
14451445- target: prop,
16381638+ target: target,
14461639 });
14471640 }
14481641 }
···1507170015081701 private getUnionReference(union: Union): string | null {
15091702 return this.getReference(union, union.name, union.namespace);
17031703+ }
17041704+17051705+ private getScalarReference(scalar: Scalar): string | null {
17061706+ // Built-in TypeSpec scalars (string, integer, boolean themselves) should not be referenced
17071707+ if (scalar.namespace?.name === "TypeSpec") return null;
17081708+17091709+ // @inline scalars should be inlined, not referenced
17101710+ if (isInline(this.program, scalar)) return null;
17111711+17121712+ // Scalars without names or namespace can't be referenced
17131713+ if (!scalar.name || !scalar.namespace) return null;
17141714+17151715+ const defName = scalar.name.charAt(0).toLowerCase() + scalar.name.slice(1);
17161716+ const namespaceName = getNamespaceFullName(scalar.namespace);
17171717+ if (!namespaceName) return null;
17181718+17191719+ // Local reference (same namespace) - use short ref
17201720+ if (
17211721+ this.currentLexiconId === namespaceName ||
17221722+ this.currentLexiconId === `${namespaceName}.defs`
17231723+ ) {
17241724+ return `#${defName}`;
17251725+ }
17261726+17271727+ // Cross-namespace reference
17281728+ return `${namespaceName}#${defName}`;
15101729 }
1511173015121731 private modelToLexiconArray(
···11+import "@typelex/emitter";
22+33+namespace com.example.scalarDefaults {
44+ /** Test default decorator on scalars */
55+ model Main {
66+ /** Uses string scalar with default */
77+ mode?: Mode;
88+99+ /** Uses integer scalar with default */
1010+ limit?: Limit;
1111+1212+ /** Uses boolean scalar with default */
1313+ enabled?: Enabled;
1414+ }
1515+1616+ /** A string type with a default value */
1717+ @default("standard")
1818+ @maxLength(50)
1919+ scalar Mode extends string;
2020+2121+ /** An integer type with a default value */
2222+ @default(50)
2323+ @minValue(1)
2424+ @maxValue(100)
2525+ scalar Limit extends integer;
2626+2727+ /** A boolean type with a default value */
2828+ @default(true)
2929+ scalar Enabled extends boolean;
3030+}