Functions
Functions are an experimental TypeSpec feature. The API and behavior of functions may reasonably be expected to change in
future releases as we gather feedback. If you choose to use functions in your TypeSpec libraries and programs, please be
aware that you may need to make adjustments to your code when updating to new versions of TypeSpec. Declaring a function
will yield a warning (you may suppress the warning with #suppress "experimental-feature").
Functions in TypeSpec allow library developers to compute and return types or values based on their inputs. Compared to decorators, functions provide an input-output based approach to creating type or value instances, offering more flexibility than decorators for creating new types dynamically. Functions enable complex type manipulation, filtering, and transformation.
Functions are declared using the fn keyword (with the required extern modifier, like decorators) and are backed by
JavaScript implementations. When a TypeSpec program calls a function, the corresponding JavaScript function is invoked
with the provided arguments, and the result is returned as either a Type or a Value depending on the functionâs
declaration.
Declaring functions
Section titled âDeclaring functionsâFunctions are declared using the extern fn syntax followed by a name, parameter list, optional return type constraint,
and semicolon:
extern fn functionName(param1: Type, param2: valueof string): ReturnType;Here are some examples of function declarations:
// No arguments, returns a type (default return constraint is 'unknown')extern fn createDefaultModel();
// Takes a string type, returns a typeextern fn transformModel(input: string);
// Takes a string value, returns a typeextern fn createFromValue(name: valueof string);
// Returns a value instead of a typeextern fn getDefaultName(): valueof string;
// Takes and returns valuesextern fn processFilter(filter: valueof Filter): valueof Filter;Calling functions
Section titled âCalling functionsâFunctions are called using standard function call syntax with parentheses. They can be used in type expressions, aliases, and anywhere a type or value is expected:
// Call a function in an aliasalias ProcessedModel = transformModel("input");
// Call a function for a default valuemodel Example { name: string = getDefaultName();}
// Use in template constraintsalias Filtered<T, F extends valueof Filter> = applyFilter(T, F);Return types and constraints
Section titled âReturn types and constraintsâFunctions can return either types or values, controlled by the return type constraint:
- No return type specified: Returns a
Type(implicitly constrained tounknown) valueof SomeType: Returns a value of the specified type- Mixed constraints:
Type | valueof Typeallows returning either types or values
// Returns a typeextern fn makeModel(): Model;
// Returns a string valueextern fn getName(): valueof string;
// Can return either a type or valueextern fn flexible(): unknown | (valueof unknown);Parameter types
Section titled âParameter typesâFunction parameters follow the same rules as decorator parameters:
- Type parameters: Accept TypeScript types (e.g.,
param: string) - Value parameters: Accept values using
valueof(e.g.,param: valueof string) - Mixed parameters: Can accept both types and values with union syntax
extern fn process( model: Model, // Type parameter name: valueof string, // Value parameter optional?: string, // Optional type parameter ...rest: valueof string[] // Rest parameter with values);Practical examples
Section titled âPractical examplesâType transformation
Section titled âType transformationâFunctions may be used to transform types in arbitrary ways without modifying an existing type instance. In the
following example, we declare a function applyVisibility that could be used to transform an input Model into an
output Model based on a VisibilityFilter object. We use a template alias to instantiate the new instance, because
templates cache their instances and always return the same type for the same template arguments.
// Transform a model based on a filterextern fn applyVisibility(input: Model, visibility: valueof VisibilityFilter): Model;
const READ_FILTER: VisibilityFilter = #{ any: #[Public] };
// Using a template to call a function can be beneficial because templates cache// their instances. A function _never_ caches its results, so each time `applyVisibility`// is called, it will run the underlying JavaScript function. By using a template to call// the function, it ensures that the function is only called once per unique instance// of the template.alias Read<M extends Model> = applyVisibility(M, READ_FILTER);Value computation
Section titled âValue computationâFunctions can also be used to extract complex logic. The following example shows how a function might be used to compute a default value for a given type of field. The external function can have arbitrarily complex JavaScript logic, so it can utilize any method of computing the result value that it deems appropriate.
// Compute a default value using some external logicextern fn computeDefault(fieldType: string): valueof unknown;
model Config { timeout: int32 = computeDefault("timeout");}Accepting options objects
Section titled âAccepting options objectsâYou may find it useful to accept an âoptionsâ object as a parameter to a function. In such cases, itâs a good idea to
define a model for the options structure, to provide better type safety and documentation for the expected options. You
can then use valueof to accept an instance of the options model as an argument to the function call.
/** Options for the `createDerivedModel` function. */model CreateDerivedModelOptions { /** * If set, overrides the name of the derived model. */ name?: string;}
/** * Creates a new model derived from the input model, with some optional modifications. * * @param m the input model to derive from * @param options optional parameters to control how the model is derived * @returns a new model derived from `m` with the specified options applied */extern fn createDerivedModel( m: Reflection.Model, options?: valueof CreateDerivedModelOptions): Reflection.Model;
// Example usage:model BaseModel { id: int32;}
alias DefaultDerived = createDerivedModel(BaseModel);alias CustomDerived = createDerivedModel(BaseModel, #{ name: "CustomName" });Function types
Section titled âFunction typesâA function itself is a value, and can be assigned to a constant:
extern fn example(): void;
// A function is a value.const f = example;A function cannot be assigned to an alias (though the result of calling a function can, if the function evaluates to a type):
extern fn example(): unknown;
// OK -- the function is a valueconst f = example;
// Erroralias F = example;// ~~~~~~~ > A value cannot be used as a type.
// OK -- the result of calling `example` is a type.alias T = example();As values, functions have types:
extern fn example(v: valueof string): valueof string;
// `Example` is equivalent to `fn (v: valueof string) => valueof string`alias Example = typeof example;
const f: fn(v: valueof string) => valueof string = f;Function type syntax
Section titled âFunction type syntaxâA function type is written with the âfnâ keyword, followed by a parenthesized list of parameters, and an optional return type annotation. The parameters are like decorator parameters, and consist of a name, optional ? indicating that the parameter is optional, and a type constraint (â:â followed by a type constraint). The return type annotation uses the â=>â âdouble-arrowâ sigil followed by a constraint type. Like with a function declaration, if the return type is not specified, it is implicitly unknown.
Here are some example function types:
fn(): a function that requires no arguments and returns any type (unknown).fn() => valueof unknown: a function that requires no arguments and returns any value.fn() => unknown | valueof unknown: a function that requires no arguments and returns any entity (type or value).fn(x: valueof string) => valueof string: a function that requires one string value argument and returns a string value.fn(x?: valueof string) => valueof int32: a function that optionally accepts one string value argument and returns anint32value.fn(x: valueof string, ...rest: valueof string[]) => valueof boolean: a function that requires at least one string value argument, accepts an arbitrary number of string value arguments, and returns a boolean value.fn(m: Reflection.Model) => void: a function that requires one argument that is a model type and returnsvoid.
Function type assignability
Section titled âFunction type assignabilityâFunctions may be assigned to any function type that is compatible with its call signature. In general, a function A is assignable to another function B if the arguments that can be passed to B are guaranteed to be valid arguments for A, and the return type of A is a valid return type for B.
Compared to TypeScript function types, TypeSpec function types are a little bit stricter: a rest parameter cannot satisfy a required parameter (fn (x: valueof string) is not assignable to fn (...args: valueof string[])), but it can satisfy an optional parameter (fn (x?: valueof string) is assignable to fn(...args: valueof string[])).
Here are some example functions and whether they are assignable to each other:
| Function A | Function B | Is A assignable to B? | Explanation |
|---|---|---|---|
fn () => valueof string | fn () => valueof unknown | â Yes | valueof string is assignable to valueof unknown. |
fn () => valueof unknown | fn () => valueof string | â No | valueof unknown is not assignable to valueof string. |
fn (x: valueof string) => void | fn (x: valueof unknown) => void | â No | valueof unknown is not assignable to valueof string (Function A requires a string, but Function B may be called with any value). |
fn (x: valueof unknown) => void | fn (x: valueof string) => void | â Yes | valueof string is assignable to valueof unknown (Function A may be called with any value, so it can also be called with a string). |
fn (x?: valueof string) => void | fn (...args: valueof string[]) => void | â Yes | Function A optionally takes one string argument, and Function B can be called with any number of string arguments |
fn (...args: valueof string[]) => void | fn (x?: valueof string) => void | â Yes | Function A can be called with any number of string arguments, and Function B may be called with one string argument or none. |
fn (x: valueof string) => void | fn (...args: valueof string[]) => void | â No | Function A requires one string argument, but Function B may be called with zero arguments. |
fn (...args: valueof string[]) => void | fn (x: valueof string) => void | â Yes | Function A can be called with any number of string arguments, and Function B requires one string argument. |
When to use function types
Section titled âWhen to use function typesâUse a function type to constrain a value to be a function with a specific call signature. This may be useful for decorators that accept functions as arguments, or for defining template arguments that accept functions. Ordinarily, you will not need to use function types unless you are developing a library that makes use of higher-order functions (i.e., functions or decorators that accept other functions as arguments).
Examples:
/** * Calls `f` with the given model. * * @param f An function that accepts the model. */extern dec apply(target: Reflection.Model, f: valueof fn(m: Reflection.Model) => void);
/** * Maps over an array using the provided function. * * @param arr The array of values to map over. * @param f A function that maps each item in `arr` to a new value. * @returns an array of the results of calling `f` on each item in `arr`. */extern fn map(arr: valueof unknown[], f: valueof fn(item: valueof unknown) => valueof unknown);
/** * A model that accepts a function to create a default value. * * Template parameters can be function values as well, in the same way that they can be string * or numeric values, and function typed parameters can have function values as defaults. */model MyTemplate< Props extends Reflection.Model, MakeId extends valueof fn(props: Reflection.Model) => valueof string = makeIdDefault> { id: string = MakeId(Props); ...Props;}
/** A default function to create IDs for `MyTemplate`. */extern fn makeIdDefault(props: Reflection.Model): valueof string;Note the use of valueof. Without valueof, the parameter f would be a type parameter, and would only accept a compatible function type. By using valueof, the parameter f will accept a function value (a callable function) that is compatible with the specified signature.
Implementation notes
Section titled âImplementation notesâ- Function results are never cached, unlike template instances. Calling the same function with the same arguments multiple times will result in multiple function calls.
- Functions may have side-effects when called; they are not guaranteed to be âpureâ functions. Be careful when writing functions to avoid manipulating the type graph or storing undesirable state (though there is no rule that will prevent you from doing so).
- Functions are evaluated in the compiler. If you write or utilize computationally intense functions, it will impact compilation times and may affect language server performance.
Implementing functions in your library
Section titled âImplementing functions in your libraryâSee Extending TypeSpec - Functions for more information about how to add a function to your TypeSpec library.