Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

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.

stable7 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 patternCircle 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.

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
(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…