What's the Difference Between Nominal, Structural, and Duck Typing?

Andrew (he/him) - Jan 9 '22 - - Dev Community

Photo by Ian Taylor on Unsplash

Nominal Typing

Most major programming languages with objects use nominal typing, where the name (or fully qualified class name, FQCN) of an object determines whether or not it is equal to another object, or assignable to a variable of a particular type. For example, in Java

class Dog {
  public String name;
  public Dog (String name) {
    this.name = name;
  }
}

class Person {
  public String name;
  public Person (String name) {
    this.name = name;
  }
}
Enter fullscreen mode Exit fullscreen mode

If you create a Dog and a Person with the same name, Java will tell you that they're not the same thing, even though both classes have the same structure (a single String field named name) and the same internal state (name is "Fido")

Dog dog = new Dog("Fido");
Person person = new Person("Fido");

// System.out.println(dog == person); // error: incomparable types: Dog and Person
System.out.println(dog.equals(person)); // false
System.out.println(person.equals(dog)); // false
Enter fullscreen mode Exit fullscreen mode

And you cannot pass a Dog to a method which expects a Person

class Greeter {
    public static void greet (Person person) {
        System.out.println("Hello, " + person.name + "!");
    }
}

// ...

Greeter.greet(person); // Hello, Fido!
// Greeter.greet(dog); // error: incompatible types: Dog cannot be converted to Person
Enter fullscreen mode Exit fullscreen mode

Structural Typing

In contrast, TypeScript allows for structural typing in some scenarios, where the structure of the objects is all that matters (and class names are immaterial). If we had the following two classes in TypeScript, we will see -- similar to Java -- that they are not equal when compared with ==

class Dog {
  name: string
  constructor (name: string) {
    this.name = name;
  }
}

class Person {
  name: string
  constructor (name: string) {
    this.name = name;
  }
}

const dog = new Dog("Fido");
const person = new Person("Fido");

console.log(dog == person); // false
Enter fullscreen mode Exit fullscreen mode

But suppose we write our Greeter class in TypeScript as

class Greeter {
  static greet (greetable: { name: string }) {
    console.log(`Hello, ${greetable.name}!`);
  }
}

Greeter.greet(person); // Hello, Fido!
Greeter.greet(dog); // Hello, Fido!
Enter fullscreen mode Exit fullscreen mode

TypeScript simply checks that the object passed to greet() has a name: string field, because that's the type we specified for greetable: an object with a name: string field. It doesn't care what class greetable might be, or if it has other fields and methods as well

class Bird {
  color: string
  name: string
  constructor (color: string, name: string) {
    this.color = color;
    this.name = name;
  }
}

const bird = new Bird("red", "Boyd");
Greeter.greet(bird); // Hello, Boyd!
Enter fullscreen mode Exit fullscreen mode

Duck Typing

In JavaScript, we might rewrite the above TypeScript classes like

class Dog {
  constructor (name) {
    this.name = name;
  }
}

class Person {
  constructor (name) {
    this.name = name;
  }
}

class Bird {
  constructor (color, name) {
    this.color = color;
    this.name = name;
  }
}

const dog = new Dog("Fido");
const person = new Person("Fido");
const bird = new Bird("red", "Boyd");
Enter fullscreen mode Exit fullscreen mode

But, as JavaScript doesn't specify types using the : as TypeScript does, we also have to rewrite our Greeter slightly

class Greeter {
  static greet (greetable) {
    console.log("Hello, " + greetable.name + "!");
  }
}

Greeter.greet(person); // Hello, Fido!
Greeter.greet(dog); // Hello, Fido!
Greeter.greet(bird); // Hello, Boyd!
Enter fullscreen mode Exit fullscreen mode

In this case, there are no type or structural constraints at all on greetable. JavaScript is using duck typing here, where field and method accesses are only checked at runtime (and not at compile time, because JavaScript isn't compiled). If a greetable has all of the required fields, no errors will be thrown.

However, if a field is missing...

class Complimenter {
  static compliment (target) {
    console.log("Hello, " + target.name + "!");
    console.log("What a nice shade of " + target.color + " you are!");
  }
}

Complimenter.compliment(person); // Hello, Fido! What a nice shade of undefined you are!
Complimenter.compliment(dog); // Hello, Fido! What a nice shade of undefined you are!
Complimenter.compliment(bird); // Hello, Boyd! What a nice shade of red you are!
Enter fullscreen mode Exit fullscreen mode

...we can get undefined results.

The name "duck typing" comes from the phrase "If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck". When using duck typing, we run the program as long as we can until we hit a runtime error -- until the "duck" no longer looks like a duck.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player