#Type System
XQuery has a rich, formally specified type system built on XML Schema. If you're coming from C#, many concepts will be familiar — atomic types, function types, records — but the details differ in important ways. The biggest difference: everything in XQuery is a sequence, and the type system reflects that.
#Contents
#Atomic Types
Atomic types are the building blocks. They're derived from XML Schema and prefixed with xs:.
#Commonly Used Types
|
XQuery Type |
C# Equivalent |
Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
— |
|
|
|
|
Base64-encoded binary |
|
|
|
Hex-encoded binary |
#Date and Time Types in Detail
Dates trip up many developers. XQuery follows ISO 8601 strictly:
(: Date — year, month, day :)
xs:date("2026-03-19")
(: DateTime — date plus time :)
xs:dateTime("2026-03-19T14:30:00")
(: DateTime with timezone :)
xs:dateTime("2026-03-19T14:30:00-05:00")
(: Current date/time functions :)
current-date() (: today as xs:date :)
current-dateTime() (: now as xs:dateTime :)
current-time() (: current time as xs:time :)C# parallel:
// Date string
DateTime.Now.ToString("yyyy-MM-dd") // "2026-03-19"
// DateTime string
DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss") // "2026-03-19T14:30:00"
// Timezone-aware
DateTimeOffset.Now.ToString("yyyy-MM-ddTHH:mm:sszzz") // with offset#Numeric Type Hierarchy
XQuery's numeric types form a hierarchy. This matters for arithmetic:
xs:double
↑ (promotes from)
xs:float
↑ (promotes from)
xs:decimal
↑ (subtype)
xs:integer
↑ (subtypes)
xs:long, xs:int, xs:short, xs:byte, xs:nonNegativeInteger, etc.In practice, you'll mostly use xs:integer, xs:decimal, and xs:double:
(: Integer literal :)
42
(: Decimal literal — has a dot :)
3.14
(: Double literal — has an exponent :)
3.14e0
(: Explicit construction :)
xs:integer("42")
xs:decimal("3.14")
xs:double("3.14")#Untyped Data
When you read XML without a schema, text content is xs:untypedAtomic. XQuery automatically converts untyped values during comparisons and arithmetic, but explicit casting is safer:
(: $book/price is xs:untypedAtomic from unvalidated XML :)
let $price := $book/price
(: Implicit conversion — works but fragile :)
$price > 30
(: Explicit casting — clearer, fails fast on bad data :)
xs:decimal($price) > 30
C# parallel: Like the difference between dynamic and strongly-typed properties:
// Implicit (like untyped XQuery)
dynamic price = element.Value; // risky
// Explicit (like xs:decimal cast)
decimal price = (decimal)element; // clear#Sequence Types
Every value in XQuery is a sequence. Sequence types describe what's in the sequence and how many items are allowed.
#Occurrence Indicators
|
Indicator |
Meaning |
C# Parallel |
|---|---|---|
|
(none) |
Exactly one |
|
|
|
Zero or one |
|
|
|
Zero or more |
|
|
|
One or more |
|
declare function local:example(
$required as xs:string, (: exactly one string :)
$optional as xs:string?, (: zero or one string :)
$multiple as xs:string*, (: any number of strings :)
$at-least-one as xs:string+ (: one or more strings :)
) as xs:string* { (: returns zero or more strings :)
...
};C# parallel:
IEnumerable<string> Example(
string required, // non-null
string? optional, // nullable
IEnumerable<string> multiple, // any count
IEnumerable<string> atLeastOne // non-empty (by convention)
) { ... }#Item Type Specifiers
Beyond atomic types, you can specify node types and other item types:
(: Node types :)
node() (: any node :)
element() (: any element :)
element(product) (: element named "product" :)
attribute() (: any attribute :)
attribute(id) (: attribute named "id" :)
text() (: text node :)
comment() (: comment node :)
document-node() (: document node :)
processing-instruction()(: processing instruction :)
(: General item types :)
item() (: any item — node or atomic :)
map(*) (: any map :)
array(*) (: any array :)
function(*) (: any function :)#Practical Examples
(: Accept any XML elements, return a map :)
declare function local:summarize($nodes as element()*) as map(*) {
map {
"count": count($nodes),
"names": array { distinct-values($nodes ! local-name(.)) }
}
};
(: Accept a document, return elements :)
declare function local:extract-data(
$doc as document-node()
) as element(record)* {
$doc//record
};#Function Types
Functions are first-class values in XQuery. You can declare the type of a function parameter or variable:
(: A function that takes an integer and returns a boolean :)
function(xs:integer) as xs:boolean
(: A function that takes two strings and returns a string :)
function(xs:string, xs:string) as xs:string
(: Any function :)
function(*)#Using Function Types
(: Higher-order function: filter with a custom predicate :)
declare function local:filter(
$items as item()*,
$predicate as function(item()) as xs:boolean
) as item()* {
for $item in $items
where $predicate($item)
return $item
};
(: Usage :)
local:filter(
//product,
function($p) { xs:decimal($p/price) > 50 }
)C# parallel:
IEnumerable<T> Filter<T>(IEnumerable<T> items, Func<T, bool> predicate)
=> items.Where(predicate);
Filter(products, p => p.Price > 50);#Storing Functions in Variables
let $transforms := map {
"upper": upper-case#1,
"lower": lower-case#1,
"trim": normalize-space#1
}
let $fn := $transforms?upper
return $fn("hello")
(: Result: "HELLO" :)The #1 syntax creates a named function reference. The number indicates the arity (number of arguments).
#Record Types
XQuery 4.0
Record types define the shape of a map with named, typed fields. They're XQuery's answer to C# records and TypeScript interfaces.
declare function local:create-user(
$name as xs:string,
$email as xs:string,
$age as xs:integer
) as record(name as xs:string, email as xs:string, age as xs:integer) {
map { "name": $name, "email": $email, "age": $age }
};C# parallel:
public record User(string Name, string Email, int Age);
User CreateUser(string name, string email, int age)
=> new User(name, email, age);#Optional Fields
Use ? to mark fields as optional:
declare function local:parse-address(
$node as element(address)
) as record(
street as xs:string,
city as xs:string,
state as xs:string,
zip as xs:string,
apartment as xs:string? (: optional :)
) {
map {
"street": string($node/street),
"city": string($node/city),
"state": string($node/state),
"zip": string($node/zip),
"apartment": string($node/apartment)[. ne ""]
}
};#Extensible Records
Use * to allow additional fields beyond those declared:
(: Accepts a map with at least "id" and "name", but may have more :)
declare function local:display(
$item as record(id as xs:string, name as xs:string, *)
) as element(div) {
<div id="{ $item?id }">{ $item?name }</div>
};
(: This works — extra fields are allowed :)
local:display(map {
"id": "42",
"name": "Widget",
"price": 9.99,
"category": "tools"
})#Practical Example: API Response Types
(: Define response shapes :)
declare function local:success(
$data as item()*
) as record(status as xs:string, data as item()*) {
map { "status": "ok", "data": $data }
};
declare function local:error-response(
$code as xs:integer,
$message as xs:string
) as record(status as xs:string, error as record(code as xs:integer, message as xs:string)) {
map {
"status": "error",
"error": map { "code": $code, "message": $message }
}
};
(: Usage :)
let $result := try {
let $data := collection("products")//product[@id = $id]
return if (exists($data)) then
local:success($data)
else
local:error-response(404, "Product not found")
} catch * {
local:error-response(500, $err:description)
}
return serialize($result, map { "method": "json", "indent": true() })#Enum Types
XQuery 4.0
Enum types restrict a value to a fixed set of string options. They catch invalid values at type-checking time.
declare function local:set-priority(
$task as element(task),
$priority as enum("low", "medium", "high", "critical")
) as element(task) {
copy $t := $task
modify replace value of node $t/@priority with $priority
return $t
};
(: Valid :)
local:set-priority($task, "high")
(: Type error — "urgent" is not in the enum :)
local:set-priority($task, "urgent")C# parallel:
public enum Priority { Low, Medium, High, Critical }
Task SetPriority(Task task, Priority priority) { ... }#Using Enums in Sequence Types
declare function local:filter-by-status(
$orders as element(order)*,
$status as enum("pending", "approved", "shipped", "delivered")+
) as element(order)* {
$orders[$status = @status]
};
(: Filter for multiple statuses :)
local:filter-by-status(//order, ("pending", "approved"))#Enum in Record Types
declare function local:create-ticket(
$title as xs:string,
$priority as enum("low", "medium", "high"),
$type as enum("bug", "feature", "task")
) as record(
title as xs:string,
priority as enum("low", "medium", "high"),
type as enum("bug", "feature", "task"),
created as xs:dateTime
) {
map {
"title": $title,
"priority": $priority,
"type": $type,
"created": current-dateTime()
}
};#Union Types
XQuery 4.0
Union types allow a value to be one of several types. This is useful for functions that accept different input formats.
(: Accept either a string or a date :)
declare function local:format-date(
$input as union(xs:string, xs:date, xs:dateTime)
) as xs:string {
let $date := typeswitch ($input)
case xs:date return $input
case xs:dateTime return xs:date($input)
case xs:string return xs:date($input)
default return error((), "Unexpected type")
return format-date($date, "[MNn] [D], [Y]")
};
(: All of these work :)
local:format-date(xs:date("2026-03-19"))
local:format-date(xs:dateTime("2026-03-19T10:30:00"))
local:format-date("2026-03-19")
C# parallel: C# doesn't have union types natively, but you can approximate them with method overloading or the OneOf library:
// Overloaded methods
string FormatDate(DateTime date) => date.ToString("MMMM d, yyyy");
string FormatDate(string dateStr) => FormatDate(DateTime.Parse(dateStr));#Practical Example: Flexible Input
(: A function that accepts IDs as strings or integers :)
declare function local:find-product(
$id as union(xs:string, xs:integer)
) as element(product)? {
let $str-id := string($id)
return collection("products")//product[@id = $str-id]
};
(: Both work :)
local:find-product("PRD-001")
local:find-product(42)#Union vs Sequence of item()
Don't confuse union types with item(). A union type is restrictive — it limits which types are accepted. item() accepts anything:
(: Too permissive — accepts nodes, functions, maps, anything :)
declare function local:loose($x as item()) as xs:string { ... };
(: Precise — only these three types :)
declare function local:strict($x as union(xs:string, xs:integer, xs:decimal)) as xs:string { ... };#Type Testing and Casting
XQuery provides four type-related expressions for testing, asserting, and converting types.
#instance of
Tests whether a value matches a type. Returns xs:boolean.
42 instance of xs:integer (: true :)
42 instance of xs:string (: false :)
(1, 2, 3) instance of xs:integer+ (: true :)
() instance of xs:integer? (: true — empty matches ? :)
<product/> instance of element() (: true :)
C# parallel: is:
42 is int // true
42 is string // false#Practical Use: Defensive Programming
declare function local:process($input as item()*) as item()* {
if ($input instance of element()+) then
(: Process as XML elements :)
for $e in $input return local:transform-element($e)
else if ($input instance of map(*)) then
(: Process as a map :)
local:transform-map($input)
else if ($input instance of xs:string+) then
(: Process as strings :)
$input ! upper-case(.)
else
error((), "Unsupported input type")
};#castable as
Tests whether a value can be cast to a type. Returns xs:boolean. Does not perform the cast.
"42" castable as xs:integer (: true :)
"hello" castable as xs:integer (: false :)
"2026-03-19" castable as xs:date (: true :)
"not-a-date" castable as xs:date (: false :)
C# parallel: TryParse pattern:
int.TryParse("42", out _) // true
int.TryParse("hello", out _) // false
DateTime.TryParse("2026-03-19", out _) // trueCommon pattern: safe conversion:
let $price := if ($value castable as xs:decimal) then
xs:decimal($value)
else
0.0#cast as
Converts a value to a different type. Raises an error if the conversion fails.
"42" cast as xs:integer (: 42 :)
"3.14" cast as xs:decimal (: 3.14 :)
"2026-03-19" cast as xs:date (: xs:date value :)
42 cast as xs:string (: "42" :)C# parallel: Explicit casts:
(int)"42" // InvalidCast — C# can't do this
int.Parse("42") // 42 — closer equivalent
(decimal)42 // 42m#treat as
Asserts that a value is a certain type without converting it. If the value doesn't match, it raises a type error. This is a compile-time/static-type hint, not a runtime conversion.
$value treat as xs:integer
treat as is useful when the static type checker can't infer the type, but you know it at development time:
(: The XQuery engine might not know that $data/age is always an integer :)
let $age := ($data/age) treat as xs:integer
return $age + 1
C# parallel: Somewhat like a direct cast (int)value — it asserts the type without conversion:
int age = (int)data.Age; // throws InvalidCastException if wrong type#Summary Table
|
Expression |
Purpose |
On Failure |
|---|---|---|
|
|
Test if value matches type |
Returns |
|
|
Test if value can be converted |
Returns |
|
|
Convert value to type |
Raises error |
|
|
Assert value is type (no conversion) |
Raises error |
#Type Promotion
XQuery automatically promotes (widens) certain types during operations. Understanding this prevents surprises.
#Numeric Promotion
xs:integer → xs:decimal → xs:float → xs:doubleWhen you mix numeric types in arithmetic, the narrower type promotes:
42 + 3.14 (: integer + decimal → decimal: 45.14 :)
42 + 3.14e0 (: integer + double → double: 4.514E1 :)#String/URI Promotion
xs:anyURI → xs:stringURIs can be used wherever strings are expected:
let $uri := xs:anyURI("https://example.com")
return contains($uri, "example") (: true — URI promoted to string :)#Subtype Substitution
A subtype can always be used where a supertype is expected:
(: xs:integer is a subtype of xs:decimal :)
declare function local:add-tax($amount as xs:decimal) as xs:decimal {
$amount * 1.08
};
local:add-tax(100) (: passing xs:integer where xs:decimal expected — OK :)#No Implicit Node-to-Atomic Promotion
Unlike some operations, general function calls do not automatically extract text from nodes:
(: This works — comparison atomizes automatically :)
//product/price > 50
(: This might fail — string function expects xs:string?, gets element :)
declare function local:greet($name as xs:string) as xs:string {
"Hello, " || $name
};
local:greet(//user/name) (: type error if //user/name returns an element :)
local:greet(string(//user/name)) (: correct — explicit conversion :)Be explicit about conversions from nodes to atomic values using string(), data(), or xs:decimal(). This is one of the most common sources of type errors for developers new to XQuery.