Classes define nominal types, create values, and provide a qualified scope for members.

A class declaration is written with the class name followed by a body.

💡 User (
)

The editor renders the 💡 unicode character before a class definition when one is detected.

The opening parenthesis belongs to the class declaration. Empty classes should still be written across lines when the declaration is being shown as a class, because User() reads like a call.

Members

Classes can contain fields, properties, functions, constructors, static scopes, and ghosts.

💡 User (
	name is string
	
	constructor(name is string) (
		this.name = name
	)
	
	displayName() is string (
		return name
	)
)

Typical uses the word function for functions declared inside classes. The language does not require a separate method concept for the ordinary case.

Fields may declare their type directly.

💡 User (
	name is string
)

A field type may also be inferred when the field is definitely assigned by the constructor.

💡 User (
	name
	
	constructor(name is string) (
		this.name = name
	)
)

Properties, constructors, and ghosts are introduced here as class members, but their detailed rules belong to their own pages.

Instances

Calling a class creates an instance value. Unlike TypeScript, there is no new keyword.

user = User("Ada")

Classes are reference types by default. Primitive classes define value types, and they live in a separate type universe from reference classes.

Inheritance

Inheritance uses the is keyword.

💡 Animal (
	eat() (
		console.log("animal eats")
	)
)

💡 Bunny is Animal (
)

Bunny is Animal means Bunny is a nominal subtype of Animal.

Typical inheritance is intentionally simple. Under the covers, inherited members behave like structural embedding: the members from the parent classes are copied into the child class. The child still retains the nominal history of the inheritance graph, so a value can be viewed through any inherited nominal type.

This model is sometimes useful to think of as Object.assign() style inheritance, with nominal history preserved.

💡 Animal (
	eat() (
		console.log("animal eats")
	)
)

💡 Bunny is Animal (
	eat() (
		super.eat()
		console.log("bunny eats")
	)
)

animal = Animal()
animal.eat()

bunny = Bunny()
bunny.eat()

foo is Animal = Bunny()
foo.eat()

(foo is Bunny).eat()

The calls above behave like this:

animal.eat() // "animal eats"
bunny.eat() // "animal eats", "bunny eats"
foo.eat() // "animal eats"
(foo is Bunny).eat() // "animal eats", "bunny eats"

There is no virtual dispatch. A value accessed through Animal uses the Animal member. A value accessed through Bunny uses the Bunny member.

The expression (foo is Bunny) creates an implicit short-circuiting cast. If foo is not a Bunny, the cast path does not continue.

Overrides

When a child class declares a member with the same name as an inherited member, it overrides that member for the child type.

💡 Animal (
	eat() (
		console.log("animal eats")
	)
)

💡 Bunny is Animal (
	✏️ eat() (
		super.eat()
		console.log("bunny eats")
	)
)

Overrides are inferred by matching an inherited member. In the editor, an override is rendered with a ✏️ marker before its declaration.

Override signatures may widen or narrow parameter and return types. This is sound because override dispatch is not polymorphic. The member selected by a call is determined by the type through which the value is being accessed.

Multiple Inheritance

Typical supports multiple inheritance.

💡 Animal (
	eat() (
		console.log("animal eats")
	)
)

💡 Pet (
	play() (
		console.log("pet plays")
	)
)

💡 Bunny is Animal, Pet (
)

Multiple inheritance is part of the language because Typical treats real-world classification as important. A type can be more than one kind of thing.

All class members are inherited: fields, properties, functions, constructors, ghosts, and static members.

Parent order matters. When multiple parents provide members with the same name, later inherited members are assigned into the child after earlier inherited members.

💡 Animal (
	name() is string (
		return "animal"
	)
)

💡 Pet (
	name() is string (
		return "pet"
	)
)

💡 Bunny is Animal, Pet (
)

In this example, Bunny receives members from Animal, then members from Pet.

Duplicate member names are allowed. They are not an error by default. Since inherited members retain their nominal history, the inherited version can still be reached by viewing the value through the corresponding parent type.

bunny = Bunny()

bunny.name()
(bunny is Animal).name()
(bunny is Pet).name()

This is also how Typical avoids the diamond inheritance problem. Inheritance does not depend on a hidden virtual method table or a single ambiguous runtime slot. The inherited members are copied through the inheritance graph, and their nominal origin remains available when a program needs to select a specific ancestor view.

Static Scopes

Classes can contain static scopes.

💡 Animal (
	static (
		population is int = 0
	)
)

Inside an instance function, static members are accessed through the static scope.

💡 Animal (
	static (
		population is int = 0
	)
	
	count() is int (
		return static.population
	)
)

When accessing static members from somewhere else, qualify the access through the class value.

count = Animal.population

Static members do not have an implicit instance. They can only access instance members when an instance value is available.

Static Inheritance

Classes can inherit from static classes.

💡 Factory is static (
	create() (
		return Bunny()
	)
)

💡 Bunny is Factory (
)

Static scopes can also declare their own inheritance.

💡 Animal (
	static (
		population is int = 100
	)
)

💡 Factory (
	create() (
		return Bunny()
	)
)

💡 Bunny is Animal (
	static is Factory (
	)
)

start (
	Bunny.create()
)

The static side of a class receives static inheritance that passes through the instance side, and it can also add its own static inheritance through static is.

Related Pages

More detailed class topics are covered separately: