typespec-functions

TypeSpec functions (1.10+) — extern fn declarations, JS implementations via $functions, function types, type transforms, value computation, higher-order functions

hafley66/claude-research1 installsMITSynced Aug 27

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI

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 positions

More General & Other skills

← All General & Other skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY