From bf7727c78d5c937207b9b3f9b568200007bb43a9 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Thu, 2 May 2024 09:48:30 -0700 Subject: [PATCH 1/4] add more docs --- docs/language-basics/scalars.md | 16 ++++++++- docs/language-basics/values.md | 61 +++++++++++++++++++++++++++++---- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/docs/language-basics/scalars.md b/docs/language-basics/scalars.md index 78a5009c8e..13b295ece8 100644 --- a/docs/language-basics/scalars.md +++ b/docs/language-basics/scalars.md @@ -22,9 +22,23 @@ scalar Password extends string; ## Scalars with template parameters -Scalars can also support template parameters. However, it's important to note that these templates are primarily used for decorators. +Scalars can also support template parameters. These template parameters are primarily used for decorators. ```typespec @doc(Type) scalar Unreal; ``` + +## Scalar initializers + +Scalars can be declared with an initializer for creating specific scalar values based on other values. For example: + +```typespec +scalar ipv4 extends string { + init fromInt(value: uint32); +} + +const homeIp = ipv4.fromInt(2130706433); +``` + +Initializers do not have any runtime code associated with them. Instead, they merely record the scalar initializer invoked along with the arguments passed so that emitters can construct the proper value when needed. diff --git a/docs/language-basics/values.md b/docs/language-basics/values.md index 1877d92fd6..8a18c22a94 100644 --- a/docs/language-basics/values.md +++ b/docs/language-basics/values.md @@ -5,11 +5,15 @@ title: Values # Values -TypeSpec can define values in addition to types. Values are useful in an API description to define default values for types or provide example values. They are also useful when passing data to decorators, and for template parameters that are ultimately passed to decorators. +TypeSpec can define values in addition to types. Values are useful in an API description to define default values for types or provide example values. They are also useful when passing data to decorators, and for template parameters that are ultimately passed to decorators or used as default values. -There are three kinds of values: objects, arrays, and scalars. These values can be created with object literals, array literals, and scalar literals and initializers. Additionally, values can result from referencing enum members and union variants. +Values cannot be used as types, and types cannot be used as values, they are completely separate. However, string, number, boolean, and null literals can be either a type or a value depending on context (see also [scalar literals](#scalar-literals)). Additionally, union and enum member references may produce a type or a value depending on context (see also [enum member & union variant references](#enum-member--union-variant-references)). -## Object values +## Value kinds + +There are four kinds of values: objects, arrays, scalars. and null. These values can be created with object literals, array literals, scalar literals and initializers, and the null literal respectively. Additionally, values can result from referencing enum members and union variants. + +### Object values Object values use the syntax `#{}` and can define any number of properties. For example: @@ -27,7 +31,7 @@ const example = #{ } ``` -## Array values +### Array values Array values use the syntax `#[]` and can define any number of items. For example: @@ -48,11 +52,11 @@ const exampleTags1: Tags = #["TypeSpec", "JSON"]; // ok const exampleTags2: Tags = #["TypeSpec", "JSON", "OpenAPI"]; // error ``` -## Scalar values +### Scalar values There are two ways to create scalar values: with a literal syntax like `"string value"`, and with a scalar initializer like `utcDateTime.fromISO("2020-12-01T12:00:00Z")`. -### Scalar literals +#### Scalar literals The literal syntax for strings, numerics, booleans and null can evaluate to either a type or a value depending on the surrounding context of the literal. When the literal is in _type context_ (a model property type, operation return type, alias definition, etc.) the literal becomes a literal type. When the literal is in _value context_ (a default value, property of an object value, const definition, etc.), the literal becomes a value. When the literal is in an _ambiguous context_ (e.g. an argument to a template or decorator that can accept either a type or a value) the literal becomes a value. The `typeof` operator can be used to convert the literal to a type in such ambiguous contexts. @@ -67,7 +71,7 @@ extern dec setNumberTypeOrValue(target: unknown, color: numeric | (valueof numer model A {} ``` -### Scalar initializers +#### Scalar initializers Scalar initializers create scalar values by calling an initializer with one or more values. Scalar initializers for types extended from `numeric`, `string`, and `boolean` are called by adding parenthesis after the scalar reference: @@ -86,6 +90,49 @@ scalar ipv4 extends string { const ip = ipv4.fromInt(2341230); ``` +#### Null values + +Null values are created with the `null` literal. + +```typespec +const value: string | null = null; +``` + +The `null` value, like the `null` type, doesn't have any special behavior in the TypeSpec language. It is just the value `null` like that in JSON. + +## Const declarations + +Const declarations allow storing values in a variable for later reference. Const declarations have an optional type annotation. When the type annotation is absent, the type is inferred from the value by constructing an exact type from the initializer. + +```typespec +const stringValue: string = "hello"; +// ^-- type: string + +const oneValue = 1; +// ^-- type: 1 + +const objectValue = #{ x: 0, y: 0 }; +// ^-- type: { x: 0, y: 0 } +``` + +## Validation + +TypeSpec will validate values against built-in validation decorators like `@minLength` and `@maxValue`. + +```typespec +@maxLength(3) +scalar shortString extends string; + +const s1: shortString = "abc"; // ok +const s2: shortString = "abcd"; // error: + +model Entity { + a: shortString; +} + +const e1: Entity = #{ a: "abcd" } // error +``` + ## Enum member & union variant references References to enum members and union variants can be either types or values and follow the same rules as scalar literals. When an enum member reference is in _type context_, the reference becomes an enum member type. When in _value context_ or _ambiguous context_ the reference becomes the enum member's value. From 9f9350770534676935c9e148c38f0e567f96ed7c Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Fri, 3 May 2024 16:14:55 -0700 Subject: [PATCH 2/4] finish docs --- docs/extending-typespec/basics.md | 20 ++++-- docs/extending-typespec/create-decorators.md | 70 +++++++++++--------- docs/language-basics/decorators.md | 2 +- docs/language-basics/templates.md | 43 ++++++++++++ 4 files changed, 98 insertions(+), 37 deletions(-) diff --git a/docs/extending-typespec/basics.md b/docs/extending-typespec/basics.md index 6928722ab7..af1c749488 100644 --- a/docs/extending-typespec/basics.md +++ b/docs/extending-typespec/basics.md @@ -106,7 +106,7 @@ Open `./src/lib.ts` and create your library definition that registers your libra If `$lib` is not accessible from your library package (for example, `import {$lib} from "my-library";`), some features such as linting and emitter option validation will not be available. ::: -Here's an example: +For example: ```typescript import { createTypeSpecLibrary } from "@typespec/compiler"; @@ -122,7 +122,19 @@ export const { reportDiagnostic, createDiagnostic } = $lib; Diagnostics are used for linters and decorators, which are covered in subsequent topics. -### f. Create `index.ts` +### f. Set package flags + +You can optionally set any package flags by exporting a `$flags` const that is initialized with the `definePackageFlags`. Like `$lib`, this value must be exported from your package. + +It is strongly recommended to set `valueMarshalling` to `"new"` as this will be the default behavior in future TypeSpec versions. + +```typescript +export const $flags = definePackageFlags({ + valueMarshalling: "new", +}); +``` + +### g. Create `index.ts` Open `./src/index.ts` and import your library definition: @@ -131,7 +143,7 @@ Open `./src/index.ts` and import your library definition: export { $lib } from "./lib.js"; ``` -### g. Build TypeScript +### h. Build TypeScript TypeSpec can only import JavaScript files, so any changes made to TypeScript sources need to be compiled before they are visible to TypeSpec. To do this, run `npx tsc -p .` in your library's root directory. If you want to re-run the TypeScript compiler whenever files are changed, you can run `npx tsc -p . --watch`. @@ -148,7 +160,7 @@ Alternatively, you can add these as scripts in your `package.json` to make them You can then run `npm run build` or `npm run watch` to build or watch your library. -### h. Add your main TypeSpec file +### i. Add your main TypeSpec file Open `./lib/main.tsp` and import your JS entrypoint. This ensures that when TypeSpec imports your library, the code to define the library is run. When we add decorators in later topics, this import will ensure those get exposed as well. diff --git a/docs/extending-typespec/create-decorators.md b/docs/extending-typespec/create-decorators.md index 717c917f1e..c4d5275e01 100644 --- a/docs/extending-typespec/create-decorators.md +++ b/docs/extending-typespec/create-decorators.md @@ -35,7 +35,7 @@ using TypeSpec.Reflection; extern dec track(target: Model | Enum); ``` -### Optional parameters +## Optional parameters You can mark a decorator parameter as optional using `?`. @@ -43,7 +43,7 @@ You can mark a decorator parameter as optional using `?`. extern dec track(target: Model | Enum, name?: valueof string); ``` -### Rest parameters +## 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`. @@ -51,28 +51,25 @@ You can prefix the last parameter of a decorator with `...` to collect all the r extern dec track(target: Model | Enum, ...names: valueof string[]); ``` -## Requesting a value type +## Value parameters -It's common for decorator parameters to expect a value (e.g., a string or a number). However, using `: string` as the type would also allow a user of the decorator to pass `string` itself or a custom scalar extending string, as well as a union of strings. Instead, the decorator can use `valueof ` to specify that it expects a value of that kind. - -| Example | Description | -| ----------------- | ----------------- | -| `valueof string` | Expects a string | -| `valueof float64` | Expects a float | -| `valueof int32` | Expects a number | -| `valueof boolean` | Expects a boolean | +A decorator parameter can receive [values](../language-basics/values.md) 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](#decorator-parameter-marshalling) rules. ```tsp extern dec tag(target: unknown, value: valueof string); -// bad +// error: string is not a value @tag(string) -// good -@tag("This is the tag name") +// ok, a string literal can be a value +@tag("widgets") + +// ok, passing a value from a const +const tagName: string = "widgets"; +@tag(tagName) ``` -## Implement the decorator in JavaScript +## JavaScript decorator implementation Decorators can be implemented in JavaScript by prefixing the function name with `$`. A decorator function must have the following parameters: @@ -89,7 +86,7 @@ export function $logType(context: DecoratorContext, target: Type, name: valueof } ``` -Or in pure JavaScript: +Or in JavaScript: ```ts // model.js @@ -113,26 +110,35 @@ model Dog { ### Decorator parameter marshalling -For certain TypeSpec types (Literal types), the decorator does not receive the actual type but a marshalled value if the decorator parameter type is a `valueof`. This simplifies the most common cases. +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 Type | Marshalled value in JS | -| ----------------- | ---------------------- | -| `valueof string` | `string` | -| `valueof numeric` | `number` | -| `valueof boolean` | `boolean` | +:::note +This behavior depends on the value of the `valueMarshalling` [package flag](../extending-typespec/basics.md#f-set-package-flags). This section describes the behavior when `valueMarshalling` is set to `"new"`. In a future release this will become the default value marshalling so it is strongly recommended to set this flag. But for now, the default value marshalling is `"legacy"` which is described in the next section. In a future release the `valueMarshalling` flag will need to be set to `"legacy"` to keep the previous marshalling behavior, but the flag will eventually be removed entirely. +::: -For all other types, they are not transformed. +| TypeSpec value type | Marshalled type in JS | +| ------------------- | --------------------------------- | +| `string` | `string` | +| `boolean` | `boolean` | +| `numeric` | `Numeric` or `number` (see below) | +| `null` | `null` | +| enum member | `EnumMemberValue` | -Example: +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`. -```ts -export function $tag( - context: DecoratorContext, - target: Type, - stringArg: string, // Here instead of receiving a `StringLiteral`, the string value is being sent. - modelArg: Model // Model has no special handling so we receive the Model type -) {} -``` +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 + +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 diff --git a/docs/language-basics/decorators.md b/docs/language-basics/decorators.md index dc8a64dda4..aa57714ad5 100644 --- a/docs/language-basics/decorators.md +++ b/docs/language-basics/decorators.md @@ -61,4 +61,4 @@ model Dog { ## Creating decorators -_For more information on creating decorators, see the [Creating Decorators Documentation](../extending-typespec/create-decorators.md)._ +For more information on creating decorators, see [Creating Decorators](../extending-typespec/create-decorators.md). diff --git a/docs/language-basics/templates.md b/docs/language-basics/templates.md index 5868f8b554..2d67c3d8e6 100644 --- a/docs/language-basics/templates.md +++ b/docs/language-basics/templates.md @@ -108,3 +108,46 @@ alias Example3 = Test< Since template arguments can be specified by name, the names of template parameters are part of the template's public API. **Renaming a template parameter may break existing specifications that use the template.** **Note**: Template arguments are evaluated in the order the parameters are defined in the template _definition_, not the order in which they are written in the template _instance_. While this is usually inconsequential, it may be important in some cases where evaluating a template argument may trigger decorators with side effects. + +## Templates with values + +Templates can be declared to accept values using a `valueof` constraint. This is useful for providing default values and parameters for decorators that take values. + +```typespec +alias TakesValue = { + @doc(StringValue) + property: StringType; +}; + +alias M1 = TakesValue<"a", "b">; +``` + +When a passing a literal or an enum or union member reference directly to a template for a template parameter that accepts either a type or a value, we pass the value. In particular, `StringTypeOrValue` is a value with the string literal type `"a"`. + +```typespec +alias TakesTypeOrValue = { + @customDecorator(StringOrValue) + property: string; +}; + +alias M1 = TakesValue<"a">; +``` + +### Template parameter value types + +When a template is instantiated with a value, the type of the value is determined based on the parameter rather than the template parameter constraint. This follows the same rules as [const declaration type inference](./values.md#const-declarations). In particular, inside the template `TakesValue`, the type of `StringValue` is the string literal type `"b"`. If we passed a `const` instead, the type of the value would be the const's type. In the following example, the type of StringValue is `"a" | "b"`. + +```typespec +alias TakesValue< + StringValue extends valueof string +> = { + @doc(StringValue) + property: string; +}; + +const str: "a" | "b" = "a"; + +alias M1 = TakesValue<"a">; +``` + +This behavior is not directly observable within TypeSpec, but when decorators and emitters consume values, they can observe the type of the value in order to for example declare an appropriate variable in a target language. From e017082ee8bab02dbd430dfbcc3d582ce9b1c5ee Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Fri, 3 May 2024 16:39:53 -0700 Subject: [PATCH 3/4] Add some notes about typeof --- docs/language-basics/templates.md | 11 ++++------- docs/language-basics/values.md | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/language-basics/templates.md b/docs/language-basics/templates.md index 2d67c3d8e6..75aae7164f 100644 --- a/docs/language-basics/templates.md +++ b/docs/language-basics/templates.md @@ -122,7 +122,7 @@ alias TakesValue alias M1 = TakesValue<"a", "b">; ``` -When a passing a literal or an enum or union member reference directly to a template for a template parameter that accepts either a type or a value, we pass the value. In particular, `StringTypeOrValue` is a value with the string literal type `"a"`. +When a passing a literal or an enum or union member reference directly as a template parameter that accepts either a type or a value, we pass the value. In particular, `StringTypeOrValue` is a value with the string literal type `"a"`. ```typespec alias TakesTypeOrValue = { @@ -135,19 +135,16 @@ alias M1 = TakesValue<"a">; ### Template parameter value types -When a template is instantiated with a value, the type of the value is determined based on the parameter rather than the template parameter constraint. This follows the same rules as [const declaration type inference](./values.md#const-declarations). In particular, inside the template `TakesValue`, the type of `StringValue` is the string literal type `"b"`. If we passed a `const` instead, the type of the value would be the const's type. In the following example, the type of StringValue is `"a" | "b"`. +When a template is instantiated with a value, the type of the value and the result of the `typeof` operator is determined based on the argument rather than the template parameter constraint. This follows the same rules as [const declaration type inference](./values.md#const-declarations). In particular, inside the template `TakesValue`, the type of `StringValue` is the string literal type `"b"`. If we passed a `const` instead, the type of the value would be the const's type. In the following example, the type of `property` in `M1` is `"a" | "b"`. ```typespec alias TakesValue< StringValue extends valueof string > = { @doc(StringValue) - property: string; + property: typeof StringValue; }; const str: "a" | "b" = "a"; - -alias M1 = TakesValue<"a">; +alias M1 = TakesValue; ``` - -This behavior is not directly observable within TypeSpec, but when decorators and emitters consume values, they can observe the type of the value in order to for example declare an appropriate variable in a target language. diff --git a/docs/language-basics/values.md b/docs/language-basics/values.md index 8a18c22a94..06a2ae4240 100644 --- a/docs/language-basics/values.md +++ b/docs/language-basics/values.md @@ -115,6 +115,21 @@ const objectValue = #{ x: 0, y: 0 }; // ^-- type: { x: 0, y: 0 } ``` +## The `typeof` operator + +The `typeof` operator returns the declared or inferred type of a value reference. Note that the actual value being stored by the referenced variable may be more specific than the declared type of the value. For example, if a const is declared with a union type, the value will only ever store one specific variant at a time, but typeof will give you the declared union type. + +```typespec +const stringValue: string = "hello"; +// typeof stringValue returns `string`. + +const oneValue = 1; +// typeof stringValue returns `1` + +const stringOrOneValue: string | 1 = 1; +// typeof stringOrOneValue returns `string | 1` +``` + ## Validation TypeSpec will validate values against built-in validation decorators like `@minLength` and `@maxValue`. From e02af655672da51b9bca53d5a56f21c862ac4479 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Fri, 3 May 2024 16:42:06 -0700 Subject: [PATCH 4/4] tweak --- docs/language-basics/templates.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/language-basics/templates.md b/docs/language-basics/templates.md index 75aae7164f..6bd402143a 100644 --- a/docs/language-basics/templates.md +++ b/docs/language-basics/templates.md @@ -133,6 +133,8 @@ alias TakesTypeOrValue = { alias M1 = TakesValue<"a">; ``` +The [`typeof` operator](./values.md#the-typeof-operator) can be used to get the declared type of a value if needed. + ### Template parameter value types When a template is instantiated with a value, the type of the value and the result of the `typeof` operator is determined based on the argument rather than the template parameter constraint. This follows the same rules as [const declaration type inference](./values.md#const-declarations). In particular, inside the template `TakesValue`, the type of `StringValue` is the string literal type `"b"`. If we passed a `const` instead, the type of the value would be the const's type. In the following example, the type of `property` in `M1` is `"a" | "b"`.