Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Object key signature is not taken into account with destructive assignment on method definition level #15660

Open
zuzusik opened this issue May 8, 2017 · 14 comments
Labels
Bug A bug in TypeScript
Milestone

Comments

@zuzusik
Copy link

zuzusik commented May 8, 2017

TypeScript Version: 2.3.2

Code

interface IMyInterface {
  onChange(changes : {[key : string] : {newValue : any, oldValue : any}})
}

class MyClass implements IMyInterface {
  onChange({config}) {
    console.log(config);
  }
}

class MyAnotherClass implements IMyInterface {
  onChange(changes) {
    const {config} = changes;

    console.log(config);
  }
}

Expected behavior:
compiled without errors

Actual behavior:

test.ts(5,7): error TS2420: Class 'MyClass' incorrectly implements interface 'IMyInterface'.
  Types of property 'onChange' are incompatible.
    Type '({config}: { config: any; }) => void' is not assignable to type '(changes: { [key: string]: { newValue: any; oldValue: any; }; }) => any'.
      Types of parameters '__0' and 'changes' are incompatible.
        Type '{ [key: string]: { newValue: any; oldValue: any; }; }' is not assignable to type '{ config: any; }'.
          Property 'config' is missing in type '{ [key: string]: { newValue: any; oldValue: any; }; }'.

Note: this affects Angular >1.5 users when writing component controllers and implementing $onChanges method

@zuzusik zuzusik changed the title Object key signature is not taken into account with destructive assignment on method level Object key signature is not taken into account with destructive assignment on method definition level May 8, 2017
@mhegazy
Copy link
Contributor

mhegazy commented May 8, 2017

IMyInterface.onChange says it can be called by any object, as long as its properties have a certain type.

MyClass.onChange has a stricter requirement, saying it has to be called with an object with a property called config.

as a result MyClass is not substituatble for IMyInterface.

The second class MyAnotherClass, says it can be called with any arguments, no requirements here..

If MyClass.onChange does not care if change is not provided, consider making it optional:

class MyClass implements IMyInterface {
  onChange({config = undefined}) {
    console.log(config);
  }
}

or give it an explicit type:

class MyClass implements IMyInterface {
  onChange({config} : any) {
    console.log(config);
  }
}

you can always give type, and you would see a similar error.

class MyAnotherClass implements IMyInterface {
    onChange(changes: { config: number }) {
        const { config } = changes;

        console.log(config);
    }
}

@mhegazy mhegazy added the Question An issue which isn't directly actionable in code label May 8, 2017
@zuzusik
Copy link
Author

zuzusik commented May 8, 2017

MyClass.onChange has a stricter requirement, saying it has to be called with an object with a property called config.

But this doesn't violate typings, and TS says it does:

Property 'config' is missing in type '{ [key: string]: { newValue: any; oldValue: any; }; }'.

doesn't [key : string] mean that it can be any key there (including config)?

@mhegazy
Copy link
Contributor

mhegazy commented May 8, 2017

doesn't [key : string] mean that it can be any key there (including config)?

but it can not be there at all. MyClass.onChange says it has to have config.

@zuzusik
Copy link
Author

zuzusik commented May 8, 2017

but again, does it violate typing?
why then const {config} = changes; doesn't say the same Property 'config' is missing in type '{ [key: string]: { newValue: any; oldValue: any; }; }'.?

@mhegazy
Copy link
Contributor

mhegazy commented May 8, 2017

cause changes is declared as any.... so any thing goes. give it a type like { config: number } and you will start seeing errors. use --noImplicitAny to flag these cases.

@zuzusik
Copy link
Author

zuzusik commented May 8, 2017

ohh, I though in this case changes aren't any, but { [key: string]: { newValue: any; oldValue: any; }; } because that's how IMyInterface defines them.

should this be matched this way? we are implementing specific method, so it's arguments should be matched with types provided in interface - this sounds logical for me

@mhegazy
Copy link
Contributor

mhegazy commented May 8, 2017

implements clause does not change any thing about your class, it is just an assert that this class has to be assignable to the interface.

@zuzusik
Copy link
Author

zuzusik commented May 8, 2017

so, if type is not specified the method's it is considered to be any and so it makes method compliant with interface definition, right?

@mhegazy mhegazy added Bug A bug in TypeScript and removed Question An issue which isn't directly actionable in code labels May 8, 2017
@mhegazy
Copy link
Contributor

mhegazy commented May 8, 2017

so, if type is not specified the method's it is considered to be any and so it makes method compliant with interface definition, right?

correct. you can find more details about this in #10570

@mhegazy
Copy link
Contributor

mhegazy commented May 8, 2017

Looks like we have some issues here that need to be addressed after all...

interface IMyInterface {
  onChange(changes : {[key : string] : {newValue : any, oldValue : any}})
}

class MyClass implements IMyInterface {  // Error
    onChange({ config }) {
    }
}

class MyClass2 implements IMyInterface {   // OK
    onChange({ config }: { config: any }) {
    }
}

@mhegazy mhegazy added this to the Future milestone May 8, 2017
@zuzusik
Copy link
Author

zuzusik commented May 8, 2017

Uuufff! Glad that it helped to undercover some real issues at the end. Thanks for detailed explanation. And once again: any is evil :)

@sandersn
Copy link
Member

sandersn commented May 8, 2017

@mhegazy, I think this captures the bug you found without any destructuring. Did I understand it correctly?

interface IMyInterface {
  onChange(changes : {[key : string] : {newValue : any, oldValue : any}})
}

class MyClass2 implements IMyInterface {   // OK, but should be error
    onChange(x: { config: any }) {
    }
}

@mhegazy
Copy link
Contributor

mhegazy commented May 8, 2017

well there are a few issues here we need to understand and find a consistent place for..

these should all behave the same:

interface IMyInterface {
  onChange(changes : {[key : string] : {newValue : any, oldValue : any}})
}

class MyClass1 implements IMyInterface {   // OK, but should be error
    onChange(x: { config: any }) {
    }
}

class MyClass2 implements IMyInterface {   // OK, but should be error
    onChange({ config }) {
    }
}

class MyClass3 implements IMyInterface {   // OK, but should be error
    onChange({ config }: { config: any }) {
    }
}

arguable, remove the error to match things like:

var index: { [key: string]: { newValue: any, oldValue: any } };
var { config } = index;

var tmp = index;
var config = tmp.config;

@sandersn
Copy link
Member

sandersn commented May 8, 2017

Since this is comparing assignability of parameters, there is bivariance. So it shouldn't be an error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants