Home > other >  Can I have a class implement an interface without re-typing out all the types?
Can I have a class implement an interface without re-typing out all the types?

Time:03-14

Consider as case where we get some raw data from an API or whatnot that we expect to have a shape of ICustomer, but then we want to extend that functionality, with member functions and perhaps additional variables as part of a Customer class.

interface ICustomer {
  name: string;
  address: string;
}

class Customer implements ICustomer {
  ...
}

From this, the compiler knows that Customer will have (at least) name and address. Is there a way to have these be defined without my explicitly typing them out? Basically what I'd like to do is

class Customer implements ICustomer {
    constructor(data: ICustomer) {
       Object.assign(this, data)
    }

    someExtraFunctionality() { ... }
}

It seems silly that I would have to define each member twice.

Is this doable in some clean way? I imagine I could do some hackery with as any as Customer, but it feels there should be a better way.

CodePudding user response:

Not really, no.


You could use the class's instance type as an interface:

type WithoutFunctions<T> = {
    [K in keyof T as T[K] extends (...args: any) => any ? never : K]: T[K]
}

class Customer {
    name: string;
    address: string;

    constructor(data: WithoutFunctions<Customer>) {
       Object.assign(this, data)
    }

    someExtraFunctionality() {}
}

const customer = new Customer({ name: 'a', address: 'b' })

Here the constructor takes an object that is only the value properties, no methods, and assigns it to the instance. It requires a cumbersome utility type to strip out the functions though.


Similarly, you could use Pick to grab just the properties you want from the class type:

type ICustomer = Pick<Customer, 'name' | 'address'>

class Customer {
    name: string;
    address: string;

    constructor(data: ICustomer) {
       Object.assign(this, data)
    }

    someExtraFunctionality() {}
}

const customer = new Customer({ name: 'a', address: 'b' })

And this may give you this error:

Property 'name' has no initializer and is not definitely assigned in the constructor.(2564)

Which you can solve by adding a ! to the property in order to say that it may be undefined but you should always treat it as a string. This makes typescript not care that it doesn't ever look to be initialized.

name!: string

I wouldn't recommend that though, and this feature is fairly type unsafe and allows bugs to leak in.


But really, I think duplicating the properties is your best bet. It may be little verbose, but Typescript will verify you did it correctly, and yell at you if you didn't making this trivial to get right.

type ICustomer = {
    name: string;
    address: string;
}

class Customer implements ICustomer {
    name: string;
    address: string;
    
    constructor(data: ICustomer) {
       this.name = data.name
       this.address = data.address
    }

    someExtraFunctionality() {}
}

const customer = new Customer({ name: 'a', address: 'b' })

It's simple, it's clear, very type safe, and Typescript will light up red if you get the name of type of any of those props wrong.

CodePudding user response:

UPDATED

You can try to use type instead of interface

type CustomerType = {
  name: string;
  address: string;
}

class Customer {
    constructor(data: CustomerType) {
       Object.assign(this, data)
    }
}

OLD ANSWER

This can work with all optional attributes

data = Object.assign(this, data)

I usually assign values this way

class Customer implements ICustomer {
    constructor(data: ICustomer = {}) { //assign an empty object as a default value
       for (let key in data) {
         this[key] = data[key];
       }
    }

    function someExtraFunctionality() { ... }
}

but again, it only works if all your interface attributes are optional

interface ICustomer {
  name?: string;
  address?: string;
}

It's not simple like Object.assign but it's useful for your case.

  • Related