# Testing which class a value is

> `s is Circle` asks which type a value actually is at run time, answering by the value's own type rather than the type it is declared as. A derived value satisfies its base, `null` is of no type, and `is not` negates the test. `OfType<T>()` asks the same question of a whole set, keeping the elements that are a `T` and re-typing them. `(T)value` and `value as T` convert to the narrower type — failing loudly, or answering null.

<!-- id: class-type-tests · area: class · stability: stable · html: https://osysharp.com/reference/class/type-tests/ -->

## Summary        {#summary}
`is` asks what a value **actually** is, which is not always what it is declared as:

```osy title="asking which shape it is" test app=class-type-test
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

string WhatIsIt() {
  Shape s = new Circle { Name = "c", Radius = 2m };
  if (s is Circle) { return "circle"; }
  return "shape";
}
```

The slot says `Shape`; the value is a `Circle`, and `is` answers by the value.

## Signature      {#signature}
```osy syntax
value is Class          // true when the value's runtime type is Class, or derives from it
value is not Class      // the negation
```

## Description    {#description}

### It answers by the RUNTIME type   {#runtime-type}
That is the whole point — a declared type is what the compiler knows, and `is` is for what the compiler cannot know.
A value that really is a plain `Shape` answers false:

```osy title="the test discriminates" test app=class-type-test-negative
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

string WhatIsIt() {
  Shape s = new Shape { Name = "plain" };
  if (s is Circle) { return "circle"; }
  return "shape";
}
```

### A derived value satisfies its base   {#derived}
`is Shape` accepts the whole subtree beneath `Shape`, not only an exact `Shape`. This is what makes it a *type test*
rather than a comparison of labels:

```osy title="a Circle IS a Shape" test app=class-type-test-derived
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

string Check() {
  Shape s = new Circle { Name = "c", Radius = 2m };
  if (s is Shape) { return "yes"; }
  return "no";
}
```

### `null` is of no type   {#null}
As in C#, `null` is not an instance of anything — so `null is Circle` is false, and `null is not Circle` is true:

```osy title="null satisfies no type test" test app=class-type-test-null
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

string Check() {
  Shape? s = null;
  if (s is not Circle) { return "not-a-circle"; }
  return "circle";
}
```

### Narrowing a whole collection — `OfType<T>()`   {#oftype}
`OfType<T>()` keeps the elements that are a `T` and gives you them **as** a `T`:

```osy title="a mixed list narrowed to one type" test app=class-type-test-oftype
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

decimal TotalRadius() {
  List<Shape> shapes = new List<Shape>();
  shapes.Add(new Circle { Name = "a", Radius = 1m });
  shapes.Add(new Shape { Name = "b" });
  shapes.Add(new Circle { Name = "c", Radius = 3m });

  decimal total = 0m;
  foreach (var c in shapes.OfType<Circle>()) { total = total + c.Radius; }
  return total;                                   // 4 — the plain Shape is not in the set
}
```

It does two things at once, and the second is why you would reach for it over `Where`: it **filters** to the
elements that really are a `Circle`, and it **re-types** them, so `c.Radius` reads. `shapes.Where(s => s is Circle)`
filters identically but its result is still a `List<Shape>` statically, so a derived field is out of reach.

It only ever narrows. Asking for the element's own base, or for a type outside its hierarchy, is refused rather than
quietly widening the read or returning nothing.

### Converting to the narrower type — cast or `as`?   {#cast-and-as}
A type test answers *whether*; a **conversion** hands you the value at the narrower type. Two spellings, differing
only in what happens when the value is not that type:

```osy title="the cast and its try-conversion" test app=class-type-test-cast
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

decimal RadiusOrZero(Shape s) {
  Circle? maybe = s as Circle;              // null when it is not a Circle
  if (maybe == null) { return 0m; }
  return maybe.Radius;
}

decimal RadiusOf(Shape s) {
  Circle c = (Circle)s;                     // FAILS when it is not a Circle
  return c.Radius;
}
```

| | when it IS the type | when it is NOT |
|---|---|---|
| `(Circle)s` | the value, typed `Circle` | **fails**, naming both types |
| `s as Circle` | the value, typed `Circle?` | `null` |

Use the cast when being wrong is a bug you want to hear about, and `as` when "not a Circle" is an ordinary case you
are about to handle. That is the same advice C# gives, for the same reason.

**`null` converts to `null` under both, and neither fails.** A cast of null is not a failed cast — there is nothing
there to be of the wrong type.

Widening needs no conversion at all: a `Circle` already goes wherever a `Shape` is expected
([[class-inheritance#upcast]]).

### Dispatching on the type — `switch`   {#switch}
A `switch` arm can be a **type pattern** — `Circle c =>` — binding the value at that arm's own type:

```osy title="one arm per type" test app=class-type-test-switch
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

string Describe() {
  Shape s = new Circle { Name = "c", Radius = 2m };
  return s switch {
    Circle c => "circle:" + c.Radius.ToString(),   // `c` is a Circle here
    Shape p  => "shape:" + p.Name,
  };
}
```

The binding is **required** — write `Circle c =>`, not `Circle =>`. A bare name in pattern position is already an
enum member, and the binding is what tells the two apart.

#### Every type must be handled   {#exhaustive}
A `switch` with no `_` arm must have an arm for **every** type in the hierarchy. Leave one out and the app does not
compile, and the message names what is missing:

```osy syntax
return s switch {
  Circle c => "circle",          // ERROR: does not handle every 'Shape' — 'Shape' has no arm
};
```

This is stricter than C#, which only warns — and deliberately so. C#'s compiler cannot see hierarchies that other
assemblies might extend, so it cannot know the set is complete. An Osy# app compiles as **one unit**, so the set of
types *is* known. What that buys you is the useful half: **adding a type breaks every place that has to decide about
it**, instead of those places silently taking a default that was never considered.

Add `_ => …` when the rest really are the same — that says so explicitly, and it is not second-best.

### It works the same in the browser   {#both-sides}
A type test is decided identically wherever the code runs — in a function on the server, in a component in the
browser, and across a suspension that starts on one side and resumes on the other. There is no rule to learn about
where you may write it.

### Entities test their type too   {#entities}
`is` works on an [entity hierarchy](https://osysharp.com/reference/entity/inheritance/) as well, and reads the same. The two are decided by
different means — an entity is a row and carries its type in a column, which is what lets an entity's test run inside
a database query — but nothing about writing one differs.

## Errors         {#errors}
| you wrote | what you get |
|---|---|
| `x is Unrelated`, where the two share no hierarchy | refused — no value can be both, so the answer would be a constant you did not write |
| `x is Solo`, where `Solo` has no base and no subtypes | refused — a type test is only meaningful inside a hierarchy |
| `(Circle)s` where `s` is not a `Circle` | fails at run time, naming both types and pointing at `is` / `as` |
| `(Circle)s` where the two share no hierarchy | refused at compile time — no value can be both |
| `(Contract)order` between two ENTITY types | refused — a row already carries its type; narrow the READ with `OfType` |
| `xs.OfType<Circle>()` where `Circle` does not derive from the element type | refused — no element of the set could be one |
| `xs.OfType<Shape>()` where `Shape` is the element's BASE | refused — that would WIDEN the read, not narrow it |

## See also       {#see-also}
- [Class inheritance](https://osysharp.com/reference/class/inheritance/) — declaring the hierarchy a type test asks about
- [entity Sub : Base](https://osysharp.com/reference/entity/inheritance/) — the same question asked of rows
- [Classes](https://osysharp.com/reference/class/index/) — what a class is
