typescriptclassmethodsinterfaceimplements

Typescript incorrect type inference when class implements interface


When using composition approach in typescript over inheritance one, I want to describe my entities according to what they "can" and not what they "are". In order to do this I need to create some complex interfaces and then for my classes (I use classes in order not to create manual prototype chain and not break some optimizations that I presume exist inside js engines) to implement my interfaces. But this results in strange behavior when type of a method is not inferred correctly. On the contrary, when using objects and declaring them to be of the same interface type everything works as expected.

So I`m using VSCode with typescript 3.6.3. I`ve created interface for 2d shape that should have method to return all normals to edges. Then I create class that implements that interface and I expect it to require this method and it should have the same return type (this part works) and also same argument types (this one doesn`t). Parameter gets inferred as any. My problem is that I don`t want to create prototype chain by hand only to get consistent VSCode behavior.

Also when running tsc in console I get the same error for parameter being 'any' type in class method and expected error inside object method when accessing non-existent prop

interface _IVector2 {
  x: number;
  y: number;
}

interface _IShape2D {
  getNormals: ( v: string ) => _IVector2[];
}

export class Shape2D implements _IShape2D {
  getNormals( v ) {
    console.log( v.g );
                   ^ ----- no error here

    return [{} as _IVector2];
  }
}

export const Test: _IShape2D = {
  getNormals( v ) {
    console.log( v.g );
                   ^------ here we get expected error that  
                   ^------ 'g doesn`t exist on type string'

    return [{} as _IVector2];
  }
};

my tsconfig.json

{
  "compilerOptions": {
    "target": "es2017",
    "allowSyntheticDefaultImports": true,
    "checkJs": false,
    "allowJs": true,
    "noEmit": true,
    "baseUrl": ".",
    "moduleResolution": "node",
    "strict": true,
    "strictNullChecks": true,
    "noImplicitAny": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noFallthroughCasesInSwitch": true,
    "jsx": "react",
    "module": "commonjs",
    "alwaysStrict": true,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "noErrorTruncation": true,
    "removeComments": true,
    "resolveJsonModule": true,
    "sourceMap": true,
    "watch": true,
    "skipLibCheck": true,
    "paths": {
      "@s/*": ["./src/*"],
      "@i/*": ["./src/internal/*"]
    }
  },
  "exclude": [
    "node_modules"
  ]
}

Expected:
- class method`s parameter should be inferred as string
Actual:
- method`s parameter is inferred as any

Ultimately my question is as follows: "Is this behavior unachievable in ts and I should resort to hand written (oh dear...) prototype chains and simple objects for prototypes?"

Thank you in advance!


Solution

  • This is a design limitation in TypeScript (see ms/TS#1373). There was a fix attempted at ms/TS#6118 but it had some bad/breaking interactions with existing real-world code, so they gave up on it. There is an open issue at ms/TS#32082 asking for something better but for now there isn't anything useful.

    The suggestion at this point is to manually annotate parameter types in implementing/extending classes; this is better than resorting to hand-written prototype chains, despite being more annoying.

    export class Shape2D implements _IShape2D {
      getNormals(v: string) { // annotate here
        console.log(v.g); // <-- error here as expected
        return [{} as _IVector2];
      }
    }
    

    Note that v could indeed be any or unknown and getNormals() would be a correct implementation:

    export class WeirdShape implements _IShape2D {
      getNormals(v: unknown) { // okay
        return [];
      }
    }
    

    This is due to method parameter contravariance being type-safe... a WeirdShape is still a perfectly valid _IShape2D. So, while it would be nice for the parameter to be inferred as string, there's nothing incorrect about it being more general.

    Anyway, hope that helps; good luck!

    Link to code