Static typing

FQL supports static typing of queries and user-defined functions. Static typing identifies potential query errors before execution by validating the query shape before running the query to rule out runtime errors.

FQL verifies that the query is correct for the kinds of values that it can accept and runs the query after type checking passes. Types are inferred by usage. After a type is determined, usage must be consistent.

Types

A type is the type of a signature value and can be any of the following:

Type Description Syntax Examples

named

A named type has a name. Types, such as Boolean or String, are named types.

name

See a partial list of named types.

literal

A literal value has its own type. For example, 3 has the type 3.

literal value

1, true, { a: 3 }, "foo"

function

A function type doesn’t have a name. It has a list of arguments and a return type. For example, the function concat() has the type (other: string) => string. These types are used for anonymous functions. For example, the where() method accepts a function type as a parameter.

(arg_name: arg_type) => return_type

() => String, (start: Int, end: Int) => String

union

A union represents an OR between any number of types. For example, if a function accepts an integer or a string, it accepts the type Int | String.

type1 | type2 | type3

Int | String, Document | Null

intersection

An intersection represents an AND between any number of types, combining multiple types into a single type. For example, the types { name: string } and { address: string } can be combined into a single { name: string, address: string } type by using the { name: string } & { address: string } notation.

type1 & type2 & type3

Int & Number, { name: string } & { address: string }

The following is a partial list of named types.

Type Example value Notes

true, false

"hello world", ""

3, 9223372036854775807, 2.5

Int

3

Int is a Number type.

9223372036854775807

Long is a Number type.

2.5

Double is a Number type.

Array<A>

[1, 2, "hi", true]`

The type parameter A is the type of the values in the array.

Set<A>

Collection.all()

The type parameter A is the type of the values in the set.

Time.now()

Date("2023-04-05")

 

A type can be a concrete or generic type.

A generic type is a type that has type parameters. A type parameter is similar to a named type but isn’t known in a function signature and is, effectively, a placeholder type. The type parameter signature is a single capital letter in sequence, such as A, B, and C.

These types resolve to a concrete type after the value is used. For example, the elements in an array have the type A in signatures because an array can store any type. The type parameter A resolves to a concrete type after the array is constructed.

Signatures

A signature is the definition of a field or function in the static environment.

FQL method signatures are a simplified form of TypeScript and can be in one of the two following formats:

  • Function signatures have the form name(arg_name: arg_type) => return_type. For example, concat has the signature concat(other: String) => String. Function signatures are similar to function types, but include a name on the left.

  • Field signatures have the form name: type. For example, the field year on Date has the signature year: Int.

Type parameters act like placeholders in a signature. This means that Array<A> isn’t a static type because it has a type parameter. When you construct an array, for example, with the query [1, 2], the concrete type of that array is Array<1 | 2>, because the value of the array is determined to have the type 1 | 2.

The type parameter A is then substituted on calling functions on that array. For example, the function first() on an array has the signature first() => A | Null. This means that it returns a value that is the same type as the elements of the array, A, or a null value.

After first() is called, for example, in [1, 2].first(), the type A is resolved. In this case, first() has the concrete type () => 1 | 2 | Null because the type A resolves to 1 | 2.

Type parameters on functions are defined implicitly, in alphabetical order. For example, dbg() accepts a value and returns that same value. So, the signature is dbg(value: A) => A. The type parameter A is local to the dbg() function and is defined implicitly.

The dbg() function is called a generic function because it has type parameters.

Generic type signature

For types that include parameters, new type parameters start at B, followed by C, and continue. For example, concat() on arrays has the signature concat(other: Array<B>) => Array<A | B>. The type parameter A is the type of the array this is called on, and the type parameter B is a new type parameter local to this function.

The type Array is a generic type because it has type parameters.

Concrete type signature

A concrete type is the type of a query value. Here are some examples:

Query value Type signature

[1, 2]

Array<1 | 2>

Collection.all()

Set<CollectionDef>

Time.now

() ⇒ Time

"hello".slice(2, 4)

String

(num) ⇒ "hello".slice(num)

(num: Int) ⇒ String

UDF signature

The user-defined function (UDF) signature field can be used to define an explicitly typed signature for a UDF. For example,

Function.create({
  name: "TypeTest",
  signature: "(x: Number, y: Number) => Number",
  body: "(x, y) => x + y "
})
{
  name: "TypeTest",
  coll: Function,
  ts: Time("2023-06-15T16:26:49.410Z"),
  body: "(x, y) => x + y ",
  signature: "(x: Number, y: Number) => Number"
}

Enable and disable type checking

Scope Property Description

Database

typechecked

Enable or disable type checking for the database:
true = Enable type checking.
false = (default) Disable type checking.

Query

typecheck

Enable or disable type checking per query:
true = Enable type checking.
false = (default) Disable type checking.

If type checking is enabled for the driver or per query, type checking must also be enabled in the database.

Setting the typechecked property in the driver, query, or Dashboard overrides the database setting.

Type checking is performed on user-defined function (UDF) definitions and can’t be disabled. If a UDF definition fails type checking it results in a QueryRuntimeError. This differs from the compile-time TypeCheckError returned by a query type error.

Disabling type checking can reduce the query time, and the number of query check phase compute operations but can allow errors to present as runtime errors.

Enable database type checking

Enabled type checking for a child database by updating the database definition using the update() method:

Database.byName( "myChildDB" ).update( { typechecked: true } )

Setting the typecheck property using the Dashboard, driver, or query option overrides the database setting.

Enable query type checking

Drivers for the supported languages can be configured to enable type checking at two levels:

  • Enable type checking of all queries sent by the driver by setting the typecheck property to true in the driver client configuration.

  • Enable type checking per-query by setting the typecheck property to true in the query options field.

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!