typespec-functions
TypeSpec functions (1.10+) — extern fn declarations, JS implementations via $functions, function types, type transforms, value computation, higher-order functions
Works with
Agent Skills format with YAML frontmatter. Claude Code reads it as-is.
---
name: "typespec-functions"
description: "TypeSpec functions (1.10+) — extern fn declarations, JS implementations via $functions, function types, type transforms, value computation, higher-order functions"
license: "MIT"
---
## What I do
- Declare functions with `extern fn` that compute and return types or values
- Implement function JS backends via `$functions` export (not bare exports)
- Use function types (`fn(x: valueof string) => valueof string`) for higher-order patterns
- Transform types without mutating existing instances (replaces mutative decorators)
- Compute default values with arbitrary JS logic
- Accept options objects with `valueof` for type-safe configuration
- Pass functions as arguments to decorators and templates
## When to use me
Use this when creating type transforms, computing values at check-time, replacing mutative decorators like `@withVisibilityFilter`, or building libraries that need input-output style type manipulation. Trigger on: "extern fn", "typespec function", "function type", "$functions", "type transform", "compute default", "FunctionContext".
## Status
Experimental in TypeSpec 1.10.0 (March 2025). Declaring a function yields a warning suppressible with `#suppress "experimental-feature"`.
## Core patterns
### Declaring functions
```tsp
// No args, returns unknown (default return constraint)
extern fn createDefaultModel();
// Takes a type, returns a type
extern fn transformModel(input: string);
// Takes a value, returns a type
extern fn createFromValue(name: valueof string);
// Returns a value
extern fn getDefaultName(): valueof string;
// Takes and returns values
extern fn processFilter(filter: valueof Filter): valueof Filter;
// Optional + rest parameters
extern fn process(
model: Model, // Type parameter
name: valueof string, // Value parameter
optional?: string, // Optional type parameter
...rest: valueof string[] // Rest parameter with values
);
```
### Calling functions
```tsp
// In an alias (function result is a type)
alias ProcessedModel = transformModel("input");
// As a default value
model Example {
name: string = getDefaultName();
}
// With template caching wrapper
// Functions never cache results. Templates do. Wrap function calls
// in template aliases when the same transform will be called repeatedly.
alias Read<M extends Model> = applyVisibility(M, READ_FILTER);
```
### Functions are values, not types
```tsp
extern fn example(): unknown;
// OK: function is a value, assign to const
const f = example;
// Error: a value cannot be used as a type
alias F = example;
// OK: the result of calling a function is a type
alias T = example();
```
### Type transformation (replacing mutative decorators)
```tsp
extern fn applyVisibility(input: Model, visibility: valueof VisibilityFilter): Model;
const READ_FILTER: VisibilityFilter = #{ any: #[Public] };
// Template wrapper caches the result per unique M
alias Read<M extends Model> = applyVisibility(M, READ_FILTER);
```
### Value computation
```tsp
extern fn computeDefault(fieldType: string): valueof unknown;
model Config {
timeout: int32 = computeDefault("timeout");
}
```
### Options objects
```tsp
model CreateDerivedModelOptions {
name?: string;
}
extern fn createDerivedModel(
m: Reflection.Model,
options?: valueof CreateDerivedModelOptions
): Reflection.Model;
model BaseModel { id: int32; }
alias DefaultDerived = createDerivedModel(BaseModel);
alias CustomDerived = createDerivedModel(BaseModel, #{ name: "CustomName" });
```
## Function types
Function type syntax uses `fn` keyword with `=>` for return type:
```tsp
// Examples
fn() // no args, returns unknown
fn() => valueof unknown // no args, returns any value
fn() => unknown | valueof unknown // returns type or value
fn(x: valueof string) => valueof string // string value in, string value out
fn(x?: valueof string) => valueof int32 // optional param
fn(x: valueof string, ...rest: valueof string[]) => valueof boolean // rest params
fn(m: Reflection.Model) => void // model type in, void out
```
### Getting a function's type
```tsp
extern fn example(v: valueof string): valueof string;
// typeof extracts the function type
alias Example = typeof example;
const f: fn(v: valueof string) => valueof string = example;
```
### Assignability rules
Function types use **contravariant** parameter checking (stricter than TypeScript):
- Required params must be satisfied by required params (not rest)
- Optional params may be satisfied by required, optional, or rest
- Rest params can satisfy optional params but NOT required params
- Return types are covariant (subtype OK)
| A | B | A assignable to B? | Why |
|---|---|---|---|
| `fn () => valueof string` | `fn () => valueof unknown` | Yes | string assignable to unknown |
| `fn (x: valueof unknown) => void` | `fn (x: valueof string) => void` | Yes | contravariance: B's string assignable to A's unknown |
| `fn (x: valueof string) => void` | `fn (x: valueof unknown) => void` | No | B could be called with non-string |
| `fn (x: valueof string) => void` | `fn (...args: valueof string[]) => void` | No | rest is effectively optional, can't satisfy required |
| `fn (...args: valueof string[]) => void` | `fn (x: valueof string) => void` | Yes | rest accepts the required string arg |
## Higher-order functions
Functions and decorators can accept other functions as arguments:
```tsp
// Decorator that accepts a function
extern dec apply(target: Reflection.Model, f: valueof fn(m: Reflection.Model) => void);
// Function that accepts a function (map pattern)
extern fn map(
arr: valueof unknown[],
f: valueof fn(item: valueof unknown) => valueof unknown
);
// Template with function-valued parameter and default
model MyTemplate<
Props extends Reflection.Model,
MakeId extends valueof fn(props: Reflection.Model) => valueof string = makeIdDefault
> {
id: string = MakeId(Props);
...Props;
}
extern fn makeIdDefault(props: Reflection.Model): valueof string;
```
Note: `valueof` is required to accept a callable function value. Without it, the parameter accepts a function _type_ (not callable).
## JS implementation
### $functions export (not bare exports)
```typescript
// lib.ts
import { FunctionContext } from "@typespec/compiler";
export const $functions = {
// Keys are namespace paths
"MyOrg.MyLib": {
concat,
rename,
join,
},
};
function concat(context: FunctionContext, l: string, r: string): string {
return l + r;
}
function rename(context: FunctionContext, model: Model, name?: string): Model {
if (!name || model.name === name) return model;
// create and return a new model with the given name
// ...
}
function join(context: FunctionContext, sep: string, ...rest: string[]): string {
return rest.join(sep);
}
```
### Binding to TypeSpec declarations
```tsp
// lib.tsp
import "./lib.js";
namespace MyOrg.MyLib;
extern fn concat(l: valueof string, r: valueof string): valueof string;
extern fn rename(m: Reflection.Model, name?: valueof string): Reflection.Model;
extern fn join(sep: valueof string, ...rest: valueof string[]): valueof string;
```
### FunctionContext
First argument to every JS function implementation. Provides:
- `functionCallTarget`: target node for reporting diagnostics on the call site
- `getArgumentTarget(index)`: target node for a specific argument
```typescript
function renamed(ctx: FunctionContext, model: Model, name: string): Model {
// Report diagnostic on the function call itself
reportDiagnostic({
code: "my-diagnostic-code",
target: ctx.functionCallTarget,
});
// Report on a specific argument
reportDiagnostic({
code: "my-other-code",
target: ctx.getArgumentTarget(0),
});
}
```
### Value marshalling (same as decorators)
| TypeSpec value type | JS type |
|---|---|
| `string` | `string` |
| `boolean` | `boolean` |
| `numeric` (int32, float32, etc.) | `number` |
| `numeric` (int64, uint64, decimal128, etc.) | `Numeric` |
| `null` | `null` |
| enum member | `EnumValue` |
Types are passed as-is (not marshalled).
### Void return handling
JS functions returning `undefined` are accepted for TypeSpec `void` return type. The call always evaluates to the `void` intrinsic type regardless.
## Functions vs decorators vs templates
| Aspect | Functions | Decorators | Templates |
|---|---|---|---|
| Keyword | `extern fn` | `extern dec` | (on model/op/etc) |
| Returns values | Yes | No (void only) | N/A |
| Modifies types | Creates new types | Attaches metadata | Creates new instances |
| Caching | Never cached | N/A | Always cached |
| JS binding | `$functions` export | `$decoratorName` export | N/A |
| First-class value | Yes (assignable to const) | No | No |
## Reflection API for operations
When a function receives a `Reflection.Operation`, the JS side gets the full Operation object. Key fields:
- `op.kind === 'Operation'`, `op.name`, `op.namespace`, `op.returnType`, `op.node`, `op.isFinished`, `op.entityKind`
- `op.parameters` is a **Model**, not a Map. Iterate properties via `op.parameters.properties` (a `Map<string, ModelProperty>`)
- `op.decorators` is an array; each entry has `.decorator.name` (prefixed with `$`, e.g. `"$after"`) and `.args` (array of `{value, kind}` where `value` is the actual referenced type or Operation object)
When a function receives `Reflection.Namespace`:
- `ns.operations`, `ns.namespaces`, and `ns.models` are all `Map` instances — iterable and walkable recursively
Decorators can take `Reflection.Operation` as an arg. The JS decorator receives the full Operation object:
```tsp
extern dec after(target: Reflection.Operation, ref: Reflection.Operation);
@after(Orders.mut.update.submit)
op updateInventory(order: Order): void;
// JS: (ctx, target, ref) where ref.name === "submit"
```
### Reserved keyword: `op`
`op` is a reserved keyword and cannot be used as a parameter name in `extern fn` declarations. Use `target` or any other name:
```tsp
extern fn dumpOp(op: Reflection.Operation): unknown; // FAILS — op is keyword
extern fn dumpOp(target: Reflection.Operation): unknown; // works
```
### Return type: undefined is not valid for `unknown`
A function declared as returning `unknown` that returns `undefined` from JS errors with `"returned value null not assignable to unknown"`. Functions that only inspect types should return an input arg or another valid type rather than `undefined`.
### Alias indirection required for function results used as types
Function call results cannot be used inline as type expressions. They must go through an alias first:
```tsp
alias Merged = mergeModels(A, B); // works
model C is Merged; // works (through alias)
model C is mergeModels(A, B); // FAILS
...mergeModels(A, B); // FAILS — spread also requires alias
```
### Member access on function results: bind-time vs check-time phase mismatch
Member access in TSP is symbol-based (bind time). Function results exist at check time. The name resolver walks symbol tables built by the binder. A model created in JS via `$.model.create(...)` has a `.properties` map but may lack the `.node.symbol.members` the name resolver needs. `fnResult.someProperty` fails if the return constraint is `unknown` or `Reflection.Model`.
| Pattern | Works? | Why |
|---|---|---|
| `alias X = myFn(A, B)` | yes | fn called at check time, result is a real Type |
| `model Y is X` | yes | copies properties into Y, which gets its own symbol table |
| `model Y { ...X; }` | only if return constraint has members | spread reads from the constraint type statically |
| `op foo(x: X): X` | yes | type position, no member resolution needed |
| `X.someProperty` | no (if constraint is `unknown`/`Reflection.Model`) | name resolver can't find members on the constraint |
| `addField(makeModel("a"), "b")` | yes | fn-to-fn chaining, no member access needed |
**Model literal return constraints enable member access.** If the return constraint is a model literal, the constraint is parsed and bound at bind time, so the name resolver finds its members:
```tsp
extern fn f(m: Reflection.Model): { myProperty: string };
// f(X).myProperty WORKS — constraint literal has bound symbols
```
Only works when the shape is known statically at declaration time. Fns that construct arbitrary shapes from namespace walks cannot use this.
### Spread behavior with fn results
- `unknown` return constraint: spread is a no-op (no properties to copy)
- `Reflection.Model` return constraint: spread copies Reflection.Model's own meta-properties (name, properties, etc.), not the user model's properties
- Model literal return constraint `{ a: string, b: int32 }`: spread copies `a` and `b` correctly
### Template declaration context: functions are not called
In a template declaration context (before instantiation), function calls are not executed. The compiler calls `getDefaultFunctionResult` which returns the return constraint type as a fallback. Only at instantiation time does the JS implementation run.
## Placement constraints
`fn` declarations are **namespace-level only**. They cannot appear inside model bodies or interfaces. Only `op` can be a member of an interface; models cannot contain ops either.
```tsp
// OK
namespace MyLib;
extern fn transform(m: Reflection.Model): Reflection.Model;
// ERROR — fn is not a valid model or interface member
model Foo {
extern fn helper(): string; // invalid
}
interface Bar {
extern fn helper(): string; // invalid — use op here
}
```
## Implementation notes
- Function results are never cached. Wrap in template aliases for caching.
- Functions may have side effects (no purity guarantee).
- Functions run in the compiler. Expensive logic impacts compilation and LSP performance.
- The type graph exposes functions as `functionDeclarations` on a Namespace.
- The semantic walker visits FunctionValue declarations.
- TSPD generates extern signatures for functions (like decorators).
- Calling the same function with the same arguments N times runs the JS implementation N times (no memoization at the language level).
## Open issues (as of 2026-03)
**Issue #521** -- "Calling functions in template instantiations" (open, design:needed, since 2022). Motivating case: JS functions like `getParentResource` can't be called from `.tsp` type expressions. Four approaches proposed. No resolution.
**Issue #904** -- "Unification of templates, functions, decorators & projections" (open, epic, since 2022). Goal: unified model allowing decorators in pure TSP, treating all four as functions over types. No resolution.
## Example prompts
"Create a function that transforms a model based on visibility filters"
"Implement a type computation function with JS backend"
"Define a higher-order function that accepts a mapping function"
"Replace @withVisibilityFilter with a function-based approach"
"Create a function that computes default values for model fields"
## Verification
- Run `tsp compile` to verify function declarations resolve
- Check that `$functions` export binds correctly to `extern fn` declarations
- Verify function types assignability in template constraints
- Test value marshalling with different TypeSpec value types
- Confirm diagnostics target function call site vs argument positionsMore General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.

