Osy#betaRun it on Osyrin: osyrin.com

Reference / Class

Testing which class a value is

value is <Class> value is not <Class> (<Class>)value // converts, or fails value as <Class> // converts, or null list.OfType<<Class>>()

`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.

stable9 examples compiled by CIclassinheritancetypespatterns

Summary#

is asks what a value actually is, which is not always what it is declared as:

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#

value is Class          // true when the value's runtime type is Class, or derives from it
value is not Class      // the negation

Description#

It answers by the 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:

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#

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:

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#

As in C#, null is not an instance of anything — so null is Circle is false, and null is not Circle is true:

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<T>() keeps the elements that are a T and gives you them as a T:

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?#

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:

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 typewhen it is NOT
(Circle)sthe value, typed Circlefails, naming both types
s as Circlethe 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#

A switch arm can be a type pattern — Circle c => — binding the value at that arm's own type:

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

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:

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.

The declaration pattern — is Circle c#

s is Circle c tests AND names the value at the narrower type. c exists wherever C# says it is definitely assigned — not only in an if's body:

class Shape { public string Name; public Shape? Next; }
class Circle : Shape { public decimal Radius; }

bool IsBig(Shape? s) => s != null && s is Circle c && c.Radius > 10m;     // the right of &&

bool NoRadius(Shape s) => !(s is Circle c) || c.Radius == 0m;            // the right of || when the left failed

decimal RadiusOrZero(Shape s) => s is Circle c ? c.Radius : 0m;          // a ternary's true branch

string Name(Shape s) {
  if (!(s is Circle c)) { return s.Name; }
  else { return "circle " + c.Radius.ToString(); }                       // the else of a negated test
}

decimal Guarded(Shape s) {
  if (s is not Circle c) return 0m;                                      // a guard that cannot fall through…
  return c.Radius;                                                       // …so `c` is assigned after it
}

decimal Chain(Shape first) {
  var total = 0m;
  Shape? cur = first;
  while (cur is Circle c) { cur = c.Next; total += c.Radius; }           // a loop body: `c` keeps its value
  return total;
}

After a guard clause, and in a while body, the name is a variable of its own holding the value that was tested — moving the tested variable on afterwards (cur = c.Next;) does not change it, as in C#. The pattern forms beyond a type test are on Patterns.

Where the test could have failed, the name is not in scope — a || s is Circle c binds nothing in the if's body, because the body runs when a alone was true. That is C#'s rule, and the compiler answers it the same way: an unknown name, rather than a value that silently is not a Circle.

On a type parameter — value is string s#

In a generic function or component, value is string s asks what the call bound T to. One body serves every call, so the answer comes from the CALL SITE's type rather than the value — which matters, because in the browser an enum member and a string are both text:

enum Shade { Light, Dark }

string KindOf<T>(T v) {
  if (v is string s && s.Length > 0) { return "text " + s; }
  if (v is int n) { return "number " + (n * 2).ToString(); }
  if (v is Shade) { return "shade"; }
  return "other";
}

string Check() => KindOf("a") + "," + KindOf(3) + "," + KindOf(Shade.Dark);   // "text a,number 6,shade"

The target can be a built-in type (string, int, long, bool, decimal, double, DateTime, DateOnly, Guid, …), an enum, or an entity or class — which is tested through its hierarchy, so a Doc that IS a Contract answers is Contract. null is still of no type. The same holds in a generic component (generic component).

It works the same in the browser#

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#

is works on an entity hierarchy 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#

you wrotewhat you get
x is Unrelated, where the two share no hierarchyrefused — 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 subtypesrefused — a type test is only meaningful inside a hierarchy
x is T t inside a generic CLASS, testing the class's own Trefused — no call says which type T is there; make the method generic
(Circle)s where s is not a Circlefails at run time, naming both types and pointing at is / as
(Circle)s where the two share no hierarchyrefused at compile time — no value can be both
(Contract)order between two ENTITY typesrefused — a row already carries its type; narrow the READ with OfType
xs.OfType<Circle>() where Circle does not derive from the element typerefused — no element of the set could be one
xs.OfType<Shape>() where Shape is the element's BASErefused — that would WIDEN the read, not narrow it

See also#

Related

Class inheritance

A class can derive from another class with `class Circle : Shape`, inheriting its fields and its methods to any depth…

Classes

A class is an in-memory shape — data plus the behaviour that belongs to it — and it never touches the database. That is…

entity Sub : Base

Derives one entity from another. The subtype is its own type with its own name, its own security and its own workflows…

LINQ over a local list

Query a local `List<T>`, `HashSet<T>` or `T[]` — of your own `class` values OR of plain scalars like `string[]` and…

generic component

A component with type parameters, bound at each call site from the arguments — one component that works over any enum…