5

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!

2
  • The signature of a method consists of its name and arguments, so typescript will know that you are implementing the method from the interface only if you specify the same argument types (string in you case). Commented Oct 6, 2019 at 11:05
  • well it actually infers return type correctly, even if I don`t specify any arguments. So according to this ability, why doesn`t it "understand" what parameter types should be? Commented Oct 6, 2019 at 11:36

2 Answers 2

6

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

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for your response. I had some thoughts that it can`t be that I`m the first to discover such "error" and your answer explains pretty nicely. But I would like to make sure that I got everything right. This use case has some troubles with inferring method type on a class instance but there are no such problem when we deal with concrete objects with property that happens to be a function. Why is it so? Also, I`m not sure that if I explicitly say that parameter is string, then setting it to any should be allowed. Other way around - of course, but not like you described
Contextual typing happens with explicitly annotated objects, but doesn't happen with class methods because the latter was never implemented, and when they tried it broke people's code too much. I think it's just a historical fluke and not a principled difference. There is still some hope that this will be fixed.
see this
0

As a workaround, you can use the Parameters utility type to extract the parameters from another interface. This is useful when using a single object as a function parameter (vs positional parameters) and you have methods whose arguments might change frequently.

For example:

interface Foo {
  method(args: { arg1: string, arg2: number}): void;
}

class Bar implements Foo {
  method({ arg1, arg2 }: Parameters<Foo['method']>[0]): void {
    // arg1 and arg2 types will be correctly inferred
  }
}

If you have several methods to redeclare, you can create your own utility type for a particular class:

interface Foo {
  method(args: { arg1: string, arg2: number}): void;
}

type FooArgs<Method extends keyof Foo> = Parameters<Foo[Method]>[0];

class Bar implements Foo {
  method({ arg1, arg2 }: FooArgs<'method'>): void {
    // arg1 and arg2 types will be correctly inferred
  }
}

Hope that helps.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.