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;
}
}
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
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
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
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!
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!
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");
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!
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!
...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.