Emitter framework
The emitter framework is designed to make writing TypeSpec emitters easier and to enable sharing and composing of emitter components. Based on patterns found in JS front-end libraries, the emitter framework provides a component model and a large number of built-in components for a variety of languages that make it easy to write well-formatted source text.
This document will go over the architecture of the emitter framework and help you get started trying it out in your emitter.
Overall Architecture
The emitter framework has four separate but related areas:
- alloy: a framework for code generation that provides a React-like functional component model, manages symbols and references, renders and formats source text, and does other things useful regardless of what language youâre emitting. Alloy is not part of TypeSpec, and we are developing it to be useful even if you arenât writing a TypeSpec emitter. Read the documentation for the Alloy framework.
- alloy language components: Libraries of components for various languages. For example, allows for declaring a variable in TypeScript, and then creating a reference to that variable such that any necessary imports and package.json dependencies are added. Read the documentation for these components.
- typekits: A convenient way to pull information from the TypeSpec type graph.
- emitter framework: builds off of alloy and its language components to provide TypeSpec-aware components and utilities. For example, while the alloy language component for TypeScript interfaces allows manually declaring members, the emitter framework component can take a TypeSpec data type and convert it directly into a TypeScript interface.
When you are writing a TypeSpec emitter, you will draw on components and APIs from each of these areas.
How to get started
We recommend you read the alloy documentation to understand its component model, reactivity system, formatting capabilities, and so forth. Alloyâs core concepts document is a good place to start.
Also, make sure to read the âGetting Startedâ section under the emitter basics topic. To use the framework, you will need an emitter library and a $onEmit
function.
Add necessary dependencies
There are a few dependencies needed to use the emitter framework:
npm install --save-peer @alloy-js/core @alloy-js/typescript @typespec/emitter-framework
This is in addition to the dependencies installed from the emitter template as described in emitter basics.
$onEmit basics
The main job of your onEmit function is to establish the overall structure of your emit output. This is done by using the following APIs and components:
Output
, which sets up a bunch of context, takes formatting options likeprintWidth
, registers external symbols for libraries youâre going to use, and does a few other things.SourceDirectory
which creates a source directory at a given path.SourceFile
component from@alloy-js/core
, or from an alloy language component library such as TypeScript.writeOutput
from@typespec/emitter-framework
, which renders all your emitter content and writes it to disk.
For example, your $onEmit
function could look like this:
import { Output, SourceDirectory, SourceFile } from "@alloy-js/core";import type { EmitContext } from "@typespec/compiler";import { writeOutput } from "@typespec/emitter-framework";
export async function $onEmit(context: EmitContext) { await writeOutput( <Output> <SourceDirectory path="src" /> <SourceFile path="README.md" filetype="md"> Hello world! </SourceFile> </Output>, context.emitterOutputDir, );}
If you run this emitter, you will see a src
directory and a README.md
file in your tsp-output
directory.
TypeScript Emitter Framework Components
The TypeScript emitter framework wraps components from the Alloy TypeScript component library by adding a type
prop that accepts a TypeSpec type, allowing for easy translation of TypeSpec to TypeScript:
<ClassMethod type={Operation}>
- emit a TypeSpec operation as a class method.<EnumDeclaration type={Union | Enum} />
- emit a TypeSpec union or enum as a TypeScript enum.<FunctionDeclaration type={Operation}>
- emit a TypeSpec operation as a function declaration.<InterfaceDeclaration type={Model | Interface}>
- emit a TypeSpec model or interface as a TypeScript interface type.<TypeAliasDeclaration type={Scalar} />
- emit a scalar type as a TypeScript Type Alias.<TypeDeclaration type={Type} />
- emit a TypeSpec type as an appropriate TypeScript type declaration.<TypeExpression type={Type} />
- emit a TypeSpec type as an appropriate TypeScript type expression.<UnionDeclaration type={Union | Enum} />
- emit a TypeSpec Union or Enum as a TypeScript type declaration for a union.<UnionExpression type={Union | Enum} />
- emit a TypeSpec Union or Enum as a TypeScript union expression.
There are also some additional components handy for emitting TypeScript from TypeSpec:
<ArrayExpression elementType={Type} />
- emit a TypeScript array type of the given type.<RecordExpression elementType={Type} >
- emit a TypeScript record type of the given type.
Example TypeScript emitter
This example emitter will emit TypeScript types for some TypeSpec types.
import { For, Output, SourceDirectory, SourceFile } from "@alloy-js/core";import * as ts from "@alloy-js/typescript";import type { EmitContext } from "@typespec/compiler";import { writeOutput } from "@typespec/emitter-framework";
export async function $onEmit(context: EmitContext) { // get some models from the type graph const types = getInterestingTypes(); await writeOutput( <Output> <SourceDirectory path="src"> <ts.SourceFile path="index.ts"> <For each={types}>{(type) => <ts.TypeDeclaration type={type} />}</For> </ts.SourceFile> </SourceDirectory> <SourceFile path="README.md" filetype="md"> Hello world! </SourceFile> </Output>, context.emitterOutputDir, );}
Typekits
Typekits provide convenient access to many compiler APIs that your emitter is likely to need. For example, typekits provide APIs to examine type relationships (like âis this a subtype of a stringâ), extract decorator metadata (like âgive me the documentation for this typeâ), or even create types on the fly.
Typekits are extensible, and libraries can provide additional typekits to provide APIs useful for emitters supporting that library. For example, the HTTP library provides typekits to query HTTP metadata and pull together many different pieces of metadata into a useful object model.
Using typekits
1. Import any extensions you need
If you are writing an emitter that needs HTTP metadata, place the following import somewhere in your compilation. It only needs to exist once as only its side effects are important.
// reflect on http metadataimport "@typespec/http/experimental/typekit";
2. Import $
Import $
in any source files you need to use compiler APIs:
import { $ } from "@typespec/compiler/experimental/typekit";
Any extensions youâve imported will be available on the $
object.
3. Use typekits
if ($.scalar.extendsString(maybeString)) { console.log("Have a string scalar!");}
Default typekits
The typekits that are always available are the following:
- array: reflect on and create array types
- builtin: references to all built-in types like scalars
- enum: reflect on and create enum types
- enumMember: reflect on and create enum members
- literal: reflect on and create literal types
- model: reflect on and create model types
- modelProperty: reflect on and create model properties
- operation: reflect on and create operation types
- record: reflect on and create record types
- type: generic type utilities and query metadata applicable to multiple types (e.g. docs and built-in constraints)
- union: reflect on and create union types
- unionVariant: reflect on and create union variants
- value: reflect on and create values
Http typekits
Http typekits are added with this import:
import "@typespec/http/experimental/typekit";
It adds useful methods to the default typekits:
- model: check if a model is well-known to HTTP like
HttpFile
- modelProperty: reflect on http-specific metadata applied to model properties like
@header
,@query
, etc.
It also adds the following typekits:
- httpOperation: get a useful object model for HTTP operations which contains various important bits of information like path, verb, parameters, return types, etc.
- httpPart: reflect on HTTP parts
- httpRequest: reflect on the request of HTTP operations
- httpResponse: reflect on the response of HTTP operations