Search code examples
typescript

Typescript class implements interface simplify


I'm creating my custom class AxiosError, but I thought the code seems to be redundant.

  //types.ts

  export interface IAxiosRequest{}
  export interface IAxiosResponse{}

  export interface IAxiosError{
    message:string
    request:IAxiosRequest
    response:IAxiosResponse
  }

  // error.ts

  import {AxiosRequestConfig, AxiosResponse, IAxiosError} from 'path/to/type.ts'

  export class AxiosError extends Error implements IAxiosError {
      isAxiosError = true
      request: AxiosRequest
      response: AxiosResponse
      constructor(params:IAxiosError){
         super(params.message)
         const {request,response} = params
         this.request = request
         this.response = response
         Object.setPrototypeOf(this, AxiosError.prototype)
      }
  }

  export function createdError(params:IAxiosError){
     return new AxiosError(params)
  }

I thought if one class implements some interface, there is no need to illustrate all the properties defined in the interface. I mean, if the interface has many properties, it is annoying 'cause you have to illustrate all the properties in the class again

so I hope the error.ts should be

  import { IAxiosError} from 'path/to/type.ts'

  export class AxiosError extends Error implements IAxiosError {
      isAxiosError = true
      constructor(params:IAxiosError){
         super(params.message)
         const {request,response} = params
         this.request = request
         this.response = response
         Object.setPrototypeOf(this, AxiosError.prototype)
      }
  }

  export function createdError(params:IAxiosError){
     return new AxiosError(params)
  }

but I got error. : property doesn't exist on type AxiosError

PS: I don't wanna use separate function parameters, if so when I invoke a function, I have to notice the order of the parameters

 export class AxiosError extends Error implements IAxiosError {
   constructor(message:string,public request:AxiosRequest,public response:AxiosResponse){
     super(message)
     Object.setPrototypeOf(this, AxiosError.prototype)
   }
 }

 export function createError(message:string,request:AxiosRequest,response:AxiosResponse){
    return new AxiosError(message,request,response)
 }


is there a better solution?


Solution

  • This is a longstanding open issue; see microsoft/TypeScript#10570. Some fixes for this had been attempted before but they had some bad consequences for existing real-world code.

    Currently, an implements clause just checks whether or not the class conforms to the interface; it doesn't contextually type the class in any way. Also note that TypeScript's type system is structural and not nominal, so "implements XXX" isn't even necessary:

    interface Foo { a: string, b: number }
    function acceptFoo(foo: Foo) { }
    
    class GoodFooExplicit implements Foo { a = ""; b = 1 } // okay
    acceptFoo(new GoodFooExplicit()); // okay
    
    class GoodFooImplicit { a = ""; b = 1 }
    acceptFoo(new GoodFooImplicit()); // okay
    
    class BadFooExplicit implements Foo { a = ""; } // error
    acceptFoo(new BadFooExplicit()); // error
    
    class BadFooImplicit { a = "" }
    acceptFoo(new BadFooImplicit()); // error
    

    In the above, a GoodFooImplicit is accepted by acceptFoo() despite not being declared as implements Foo. And, for that matter, a BadFooExplicit is rejected by acceptFoo() despite being declared as implements Foo. Currently, the only use of implements XXX is to provide an early warning if an instance of the class would not be assignable to the interface, so you can catch the error at the class declaration (as in BadFooExplicit) instead of having to wait until the first attempt to use an instance as if it were assignable to the interface.


    So, what can be done? Other than going to the above linked issue and giving it a 👍, or giving up, the best I can think of is to use a helper function to generate a parent class that already implements whatever type you're looking for. For example:

    class AxiosError extends ClassFor<IAxiosError>().mixParent(Error) {
      isAxiosError = true
      constructor(params: IAxiosError) {
        super(params, params.message);
        Object.setPrototypeOf(this, AxiosError.prototype)
      }
    }
    

    In the above, ClassFor<IAxiosError>() would be a class constructor which takes a single parameter of type IAxiosError and returns an instance of IAxiosError. And ClassFor<IAxiosError>().mixParent(Error) is a class constructor which takes an IAxiosError as a first parameter, and then expects the constructor arguments for the Error class, and returns an instance of IAxiosError that is also an instanceof Error.

    Because that first argument needs to be an IAxiosError, I refactored the implementaton of your constructor so that it calls super(params, params.message).

    And you don't need to write implements IAxiosError, because extends ClassFor<IAxiosError>().mixParent(Error) already establishes that.

    My implementation of ClassFor looks like this:

    function ClassFor<T extends object>() {
      function classWithParent<A extends any[], P extends object>(parent: new (...a: A) => P) {
        return class extends (parent as any) {
          constructor(obj: T, ...parentArgs: any[]) {
            super(...parentArgs);
            Object.assign(this, obj);
          }
        } as new (obj: T, ...a: A) => (T & P)
      }
      return Object.assign(classWithParent(class { }), {
        mixParent<A extends any[], P extends object>(parent: new (...a: A) => P) {
          return classWithParent(parent);
        }
      })
    }
    

    That might look complicated, but the idea is that it returns a class expression and we use type assertions to silence the compiler when it can't verify what we're doing. All the type assertions are confined to that function, though, so you could presumably stick ClassFor in a library somewhere and just use it.

    Anyway, you can verify that it works using the playground link at the bottom.


    Playground link to code