Type Tests

value is Type answers a question about the type of value and produces a bool. What the question is depends on the operand: on a sum it asks which member is active, on an optional whether a value is present, and on anything else it compares exact types at compile time.

type-test = expr "is" postfix-type
struct Plus {}
struct Minus {}
struct Number {
    value: int32;
}

type Token = Plus | Minus | Number;

func IsOperator(token: Token) -> bool {
    return token is (Plus | Minus);
}

func Count(tokens: Token[..]) -> uint {
    var numbers: uint = 0;
    for token in tokens {
        if token is Number {
            numbers += 1;
        }
    }
    return numbers;
}

What is tested

The tested type is resolved first; together with the operand's type it selects the meaning:

Operandvalue is T is true when
a sum A | B | Cthe active member is T, or one of the members of a tested subset (A | B)
an optional P?the value is present and its payload is T — or, for a sum payload, a member or subset of it
a fallible T ! E— rejected; match .Success(…) or .Failure(…) instead
any other typethe operand's type is exactly T, after aliases are resolved — decided at compile time
let reading: int32? = 7;
let missing: int32? = none;
let a = reading is int32;            // true: present
let b = missing is int32;            // false: absent

let nested: int32?? = .Some(none);
let c = nested is int32?;            // true: one level is present

let maybe: (int32 | bool)? = true;
let d = maybe is bool;               // true: present, and the payload's member is bool

let count: int32 = 7;
let e = count is int32;              // true, known before the program runs

A test inspects one optional level only. On an int32??, the payload is an int32?, so nested is int32 is rejected:

error: type 'int32' is neither the payload of optional 'int32??' nor a member of it
  help: 'is' tests one presence level; match '.Some(...)' to inspect deeper levels

A test that cannot be true is an error

A test whose answer is known to be false is a mistake, so the compiler rejects it rather than folding it to a constant:

error: type 'bool8' is not a member or subset of sum 'Minus | Number | Plus'
error: 'is int64' can never be true for a value of type 'int32'
  help: an 'is' test on a value that is not a sum or an optional compares its exact type

On a value that is neither a sum nor an optional, is compares types by name and identity, never by layout: a struct with the same fields under another name is a different type. An is test on such a value can only be true, which makes it useful mostly in generic code.

A fallible has no type to test — its question is which channel, which is a match:

error: 'is' cannot test the channel of fallible 'int32 ! E'
  help: match '.Success(...)' or '.Failure(...)' instead

is never narrows

A test does not change the static type of its operand. After token is Number succeeds, token is still the whole sum, and its member's fields are still out of reach:

if token is Number {
    PrintLine("{}", token.value); // error
}
error: type 'Minus | Number | Plus' has no field 'value'

To use the member, take it out with a pattern:

match token {
    n: Number => PrintLine("{}", n.value),
    else => {}
}

is also never consumes, moves or binds anything. A borrowed operand is tested through the borrow.

Grouping

The right operand is a postfix type, so a sum or fallible type must be grouped:

error: a sum type after 'is' must be grouped
  help: write 'value is (Plus | Minus)'

is shares its precedence with as: it binds tighter than every binary operator, so count is int32 && count > 0 needs no parentheses. Group it when it is the operand of a prefix operator: !(missing is int32).

Generic code

A test whose operand or tested type mentions a type parameter is checked again for every instantiation. value is T on a T | U stays valid when T and U are the same type and the sum collapses — it is then a plain type comparison, and true:

func IsLeft<T, U>(value: T | U) -> bool {
    return value is T;
}

IsLeft<int32, bool>(true) is false, and IsLeft<int32, int32>(5) is true.

Interfaces

is does not test whether a value implements an interface. That would be a run-time question about the value's type, and it is reported as unavailable rather than answered wrongly:

error: type test 'is Shape' is unavailable: interface checks are not implemented

Membership is not interface implementation either: knowing that every member of a sum implements an interface, or that a test succeeded, makes no method available on the sum.

See also