Skip to main content

Nominal & Structural Typing

A static type checker can use either the name (nominal typing) or the structure (structural typing) of types when comparing them against other types (like when checking if one is a subtype of another).

Nominal typing

Languages like C++, Java, and Swift have primarily nominal type systems.

// Pseudo code: nominal system
class Foo { method(input: string) { /* ... */ } }
class Bar { method(input: string) { /* ... */ } }

let foo: Foo = new Bar(); // Error!

In this pseudo-code example, the nominal type system errors even though both classes have a method of the same name and type. This is because the name (and declaration location) of the classes is different.

Structural typing

Languages like Go and Elm have primarily structural type systems.

// Pseudo code: structural system
class Foo { method(input: string) { /* ... */ } }
class Bar { method(input: string) { /* ... */ } }

let foo: Foo = new Bar(); // Works!

In this pseudo-code example, the structural type system allows a Bar to be used as a Foo, since both classes have methods and fields of the same name and type.

If the shape of the classes differ however, then a structural system would produce an error:

// Pseudo code
class Foo { method(input: string) { /* ... */ } }
class Bar { method(input: number) { /* ... */ } }

let foo: Foo = new Bar(); // Error!

We've demonstrated both nominal and structural typing of classes, but there are also other complex types like objects and functions which can also be either nominally or structurally compared. Additionally, a type system may have aspects of both structural and nominal systems.

In Flow

Flow uses structural typing for objects and functions, but nominal typing for classes.

Functions are structurally typed

When comparing a function type with a function it must have the same structure in order to be considered valid.

1type FuncType = (input: string) => void;2function func(input: string) { /* ... */ }3let test: FuncType = func; // Works!

Objects are structurally typed

When comparing an object type with an object it must have the same structure in order to be considered valid.

1type ObjType = {property: string};2let obj = {property: "value"};3let test: ObjType = obj; // Works

Classes are nominally typed

When you have two classes with the same structure, they still are not considered equivalent because Flow uses nominal typing for classes.

1class Foo { method(input: string) { /* ... */ } }2class Bar { method(input: string) { /* ... */ } }3let test: Foo = new Bar(); // Error!
3:17-3:25: Cannot assign `new Bar()` to `test` because `Bar` [1] is incompatible with `Foo` [2]. [incompatible-type]

If you wanted to use a class structurally you could do that using an interface:

1interface Interface {2  method(value: string): void;3};4
5class Foo { method(input: string) { /* ... */ } }6class Bar { method(input: string) { /* ... */ } }7
8let test1: Interface = new Foo(); // Works9let test2: Interface = new Bar(); // Works

Opaque types

You can use opaque types to turn a previously structurally typed alias into a nominal one (outside of the file that it is defined).

1// A.js2export type MyTypeAlias = string;3export opaque type MyOpaqueType = string;4
5const x: MyTypeAlias = "hi"; // Works6const y: MyOpaqueType = "hi"; // Works

In a different file:

// B.js
import type {MyTypeAlias, MyOpaqueType} from "A.js";

const x: MyTypeAlias = "hi"; // Works
const y: MyOpaqueType = "hi"; // Error! `MyOpaqueType` is not interchangable with `string`
// ^^^^ Cannot assign "hi" to y because string is incompatible with MyOpaqueType

Flow Enums

Flow Enums do not allow enum members with the same value, but which belong to different enums, to be used interchangeably.

1enum A {2  X = "x",3}4enum B {5  X = "x",6}7
8const a: A = B.X; // Error!
8:14-8:16: Cannot assign `B.X` to `a` because `B` [1] is incompatible with `A` [2]. [incompatible-type]