Is it possible to have a function accept class instances, but not accept plain objects?

Viewed 772

I'd like to write a function that will take as a parameter class instances, but not allow plain, anonymously typed objects.

For instance:

class Dog {
    constructor(name: string, breed: "husky" | "boxer") {
        this.name = name;
        this.breed = breed;
    }
    name: string;
    breed: "husky" | "boxer";
}

class Cat {
    constructor(name: string, breed: "siamese" | "persian") {
        this.name = name;
        this.breed = breed;
    }
    name: string;
    breed: "siamese" | "persian";
}

function pat(pet: NoPlainObjects) {
    document.write(pet.constructor.name);
}

pat(new Dog('Fido', 'boxer')); //works
pat(new Cat('Spot', 'persian')); //works

pat({name: 'snuffaluffagus'}); //compile error
4 Answers

Is it possible to have a function accept class instances, but not accept plain objects?

That isn't currently possible, because TypeScript uses structural sub-typing. What that means is that an object created with a class constructor is compatible with a plain old JavaScript object literal when both have compatible properties.

Here is what the docs say about type compatibility:

The basic rule for TypeScript’s structural type system is that x is compatible with y if y has at least the same members as x. To check whether y can be assigned to x, the compiler checks each property of x to find a corresponding compatible property in y... The same rule for assignment is used when checking function call arguments. [empahsis added]

Your question raises a question: why would you want a function to accept class instances and to reject plain old JavaScript objects?

You can use the pipe to allow for different types as the argument of your function.

function pat(pet: Cat | Dog) {
    document.write(pet.name);
}

Why not use a base Interface, classes and constructors for model objects are not the preferred TypeScript way.

interface Pet {
  name: string;
}

interface Dog extends Pet {
  // no need to repeat name property.
  breed: "husky" | "boxer";
}

function pat(pet: Pet) {
  document.write(pet.name);
}

const fido: Dog = { name: 'Fido', breed: 'boxer' };

pat(fido);

I would suggest you define an interface and restrict the input parameter types using that. This way you also have type safety. Also, you can enforce what the parameter class needs to have.

interface Animal { name: string, breed: string };

class Dog implements Animal {
    constructor(name: string, breed: "husky" | "boxer") {
        this.name = name;
        this.breed = breed;
    }
    name: string;
    breed: "husky" | "boxer";
}

class Cat implements Animal {
    constructor(name: string, breed: "siamese" | "persian") {
        this.name = name;
        this.breed = breed;
    }
    name: string;
    breed: "siamese" | "persian";
}

function pat(pet: Animal) {
    document.write(pet.name);
}

pat(new Dog('Fido', 'boxer')); //works
pat(new Cat('Spot', 'persian')); //works

pat({name: 'snuffaluffagus'}); //compile error
Related