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

TypeScript comparison

TypeScript is primarily structural with narrow nominal carve-outs, while Flow is structural for plain objects and functions but nominal for classes, opaque types, and Flow Enums. See Flow and TypeScript nominal typing comparison for more.

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 TypeScript 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!
Try

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
Try

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!incompatible-typeCannot assign new Bar() to test because Bar [1] is incompatible with Foo [2].
Try

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
Try

A structural object type like {method: (value: string) => void} wouldn't work in place of the interface here — object types describe object literals, not class instances. See Interfaces for instances and objects for why and the fix.

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
Try

In a different file, MyTypeAlias remains transparent but MyOpaqueType is nominal:

1// B.js — when imported, type aliases are transparent but opaque types are not2type MyTypeAlias = string;3declare opaque type MyOpaqueType;4
5const x: MyTypeAlias = "hi"; // Works6const y: MyOpaqueType = "hi"; // Error! `MyOpaqueType` is not interchangeable with `string`incompatible-typeCannot assign "hi" to y because "hi" [1] is incompatible with MyOpaqueType [2].
Try

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!incompatible-typeCannot assign B.X to a because B [1] is incompatible with A [2].
Try

See Also​

  • Classes — nominally typed: compared by class name
  • Interfaces — structurally typed: allow different classes with the same shape
  • Objects — structurally typed: compared by shape
  • Opaque Type Aliases — nominally typed: hiding the underlying type across module boundaries
  • Flow Enums — nominally typed: each enum is a distinct type