Decorators
TypeSpec decorators are implemented as JavaScript functions. The process of creating a decorator can be divided into two parts:
- Declare the decorator signature in TypeSpec (optional but recommended)
- Implement the decorator in JavaScript
Declare the decorator signature
Section titled âDeclare the decorator signatureâWhile this step is optional, it offers significant benefits:
- It enables type checking for the parameters
- It provides IDE IntelliSense
You can declare a decorator signature using the dec
keyword. Since weâre implementing the decorator in JavaScript (the only option currently), we need to use the extern
modifier as well.
extern dec logType(target: unknown, name: string);
Specifying the decorator target
Section titled âSpecifying the decorator targetâThe first parameter of the decorator represents the TypeSpec type(s) that the decorator can be applied to.
You can specify multiple potential target types using a union expression
.
using TypeSpec.Reflection;
extern dec track(target: Model | Enum);
Optional parameters
Section titled âOptional parametersâYou can mark a decorator parameter as optional using ?
.
extern dec track(target: Model | Enum, name?: valueof string);
Rest parameters
Section titled âRest parametersâYou can prefix the last parameter of a decorator with ...
to collect all the remaining arguments. The type of this parameter must be an array expression
.
extern dec track(target: Model | Enum, ...names: valueof string[]);
Value parameters
Section titled âValue parametersâA decorator parameter can receive values by using the valueof
operator. For example the parameter valueof string
expects a string value. Values are provided to the decorator implementation according the decorator parameter marshalling rules.
extern dec tag(target: unknown, value: valueof string);
// error: string is not a value@tag(string)
// ok, a string literal can be a value@tag("widgets")
// ok, passing a value from a constconst tagName: string = "widgets";@tag(tagName)
JavaScript decorator implementation
Section titled âJavaScript decorator implementationâDecorators can be implemented in JavaScript in 2 ways:
- Prefixing the function name with
$
. e.gexport function $doc(target, name) {...}
Great to get started/play with decorators - Exporting all decorators for your library using
$decorators
variable. Recommended
export const $decorators = { // Namespace "MyOrg.MyLib": { doc: docDecoratorFn, },};
A decorator implementation takes the following parameters:
1
:context
of typeDecoratorContext
2
:target
The TypeSpec type target. (Namespace
,Interface
, etc.)3+
: Any arguments of the decorators.
import type { DecoratorContext, Type } from "@typespec/compiler";
export function $logType(context: DecoratorContext, target: Type, name: string) { console.log(name + ": " + target.kind);}
Or in JavaScript:
export function $logType(context, target, name) { console.log(name + ": " + target.kind);}
The decorator can then be used like this:
import "./model.js";
@logType("Dog type")model Dog { @logType("Name type") name: string;}
Decorator parameter marshalling
Section titled âDecorator parameter marshallingâWhen decorators are passed types, the type is passed as-is. When a decorator is passed a TypeSpec value, the decorator receives a JavaScript value with a type that is appropriate for representing that value.
TypeSpec value type | Marshalled type in JS |
---|---|
string | string |
boolean | boolean |
numeric | Numeric or number (see below) |
null | null |
enum member | EnumMemberValue |
When marshalling numeric values, either the Numeric
wrapper type is used, or a number
is passed directly, depending on whether the value can be represented as a JavaScript number without precision loss. In particular, the types numeric
, integer
, decimal
, float
, int64
, uint64
, and decimal128
are marshalled as a Numeric
type. All other numeric types are marshalled as number
.
When marshalling custom scalar subtypes, the marshalling behavior of the known supertype is used. For example, a scalar customScalar extends numeric
will marshal as a Numeric
, regardless of any value constraints that might be present.
Legacy value marshalling
Section titled âLegacy value marshallingâWith legacy value marshalling, TypeSpec strings, numbers, and booleans values are always marshalled as JS values. All other values are marshalled as their corresponding type. For example, null
is marshalled as NullType
.
TypeSpec Value Type | Marshalled value in JS |
---|---|
string | string |
numeric | number |
boolean | boolean |
Note that with legacy marshalling, because JavaScript numbers have limited range and precision, it is possible to define values in TypeSpec that cannot be accurately represented in JavaScript.
String templates and marshalling
Section titled âString templates and marshallingâIf a decorator parameter type is valueof string
, a string template passed to it will also be marshalled as a string.
The TypeSpec type system will already validate the string template can be serialized as a string.
extern dec doc(target: unknown, name: valueof string);alias world = "world!";@doc("Hello ${world} ") // receive: "Hello world!"@doc("Hello ${123} ") // receive: "Hello 123"@doc("Hello ${true} ") // receive: "Hello true"model Bar {}@doc("Hello ${Bar} ") // not called error ^ String template cannot be serialized as a string.
Typescript type Reference
Section titled âTypescript type ReferenceâTypeSpec Parameter Type | TypeScript types |
---|---|
valueof string | string |
valueof numeric | number |
valueof boolean | boolean |
string | StringLiteral | TemplateLiteral | Scalar |
Reflection.StringLiteral | StringLiteral |
Reflection.TemplateLiteral | TemplateLiteral |
Adding metadata with decorators
Section titled âAdding metadata with decoratorsâDecorators can be used to register some metadata. For this, you can use the context.program.stateMap
or context.program.stateSet
to insert data that will be tied to the current execution.
â Do not save the data in a global variable.
import type { DecoratorContext, Type } from "@typespec/compiler";import type { StateKeys } from "./lib.js";
// Create a unique keyconst key = StateKeys.customName;export function $customName(context: DecoratorContext, target: Type, name: string) { // Keep a mapping between the target and a value. context.program.stateMap(key).set(target, name);
// Keep an index of a type. context.program.stateSet(key).add(target);}
export const $lib = createTypeSpecLibrary({ // ... state: { customName: { description: "State for the @customName decorator" }, },});
export const StateKeys = $lib.stateKeys;
Reporting diagnostic on decorator or arguments
Section titled âReporting diagnostic on decorator or argumentsâThe decorator context provides the decoratorTarget
and getArgumentTarget
helpers.
import type { DecoratorContext, Type } from "@typespec/compiler";import type { reportDiagnostic } from "./lib.js";
export function $customName(context: DecoratorContext, target: Type, name: string) { reportDiagnostic({ code: "custom-name-invalid", target: context.decoratorTarget, // Get location of @customName decorator in TypeSpec document. }); reportDiagnostic({ code: "bad-name", target: context.getArgumentTarget(0), // Get location of {name} argument in TypeSpec document. });}
Linking declaration and implementation
Section titled âLinking declaration and implementationâDecorator signatures are linked to the implementation of the same name in the same namespace.
import "./lib.js";extern dec customName(target: Type, name: StringLiteral);
namespace MyLib { extern dec tableName(target: Type, name: StringLiteral);}
This is linked to the following in lib.js
:
export function $customName(context: DecoratorContext, name: string) {}
export function $tableName(context: DecoratorContext, name: string) {}setTypeSpecNamespace("MyLib", $tableName);
Troubleshooting
Section titled âTroubleshootingâExtern declaration must have an implementation in JS file
Section titled âExtern declaration must have an implementation in JS fileâPotential issues:
- The JS function is not prefixed with
$
. For a decorator called@decorate
, the JS function must be called$decorate
. - The JS function is not in the same namespace as the
extern dec
. - Is the error only showing in the IDE? Try restarting the TypeSpec server or the IDE.
You can use --trace bind.js.decorator
to log debug information about decorator loading in the JS file, which should help identify the issue.