Why typescript allows narrow down the type of method parameters in the derived class?

Topics: Language Specification
Feb 28, 2014 at 3:39 PM
Why typescript allows narrow down the type of method parameters in the derived class?
I think that this is overlooked dangerous operation.

for example. see the following code.
class Base {
  v1 = "Hi!";
}

class Derived extends Base {
  v2 = "Hello!";
}

class SampleA {
  method(value: Base): Base {
    console.log(value.v1);
    return value;
  }
}

class SampleB extends SampleA {
  method(value: Derived): Derived {
    console.log(value.v2);
    return value;
  }
}

var objA: SampleA;
var objB = new SampleB();

objA = objB;
objA.method(new Base()); // Base instance passed to SampleB#method method! but it expected Derived instance...
in TypeScript playground -> http://goo.gl/hF6WUn

$ tsc sample.ts
$ echo $?
0

I guess derived class knows the contract of the base class, there is a duty to protect it.
therefore TypeScript allows me to narrow down types of method parameters. isn't it?
Coordinator
Feb 28, 2014 at 6:27 PM
This is indeed unsound, but intentionally so. A primary motivation for this behavior is APIs that look like this:
// Library
function on(eventName: string, handler: (e: Event) => void) { ... }

// User code
on('mousemove', (e: MouseEvent) => { console.log('Mouse at ' + e.position; });
Without the bivariance on argument types, the on call would require an awkwardly large type assertion on the function expression.
Mar 1, 2014 at 2:22 AM
Thank you your answer!
I was convinced.
thank you!
Mar 3, 2014 at 11:32 AM
Edited Mar 3, 2014 at 11:33 AM
Now that there are overloads based on string constants, are there situations where this feature is still necessary? Isn't it possible to just declare different type signatures for the different string constants?
Mar 12, 2014 at 2:36 AM
Regarding : https://typescript.codeplex.com/workitem/2282 (In the comic example Civilian does not have name)

Quoting:
// Library
function on(eventName: string, handler: (e: Event) => void) { ... }

// User code
on('mousemove', (e: MouseEvent) => { console.log('Mouse at ' + e.position; });
MouseEvent has all the members of Event. Same for @vvakame's example : Derived has all the members of Base.

To build on vvakame's example the following should be an error :
class Base {
  v1 = "Hi!";
}
class Derived  {
}

class SampleA {
  method(value: Base): Base {
    return value;
  }
}

class SampleB extends SampleA {
  method(value: Derived): any { // Parameter value is not compatible with `Base`
    return value;
  }
}
Mar 12, 2014 at 10:40 AM
@RyanCavanaugh I am not convinced. If you kept it sound then all we'd need is a cast – which is the right way to do it.
// Library
function on(eventName: string, handler: (e: Event) => void) { ... }

// User code
on('mousemove', (e: Event) => { console.log('Mouse at ' + (<MouseEvent>e).pageX; });
This way it is up to the user to make sure the cast is valid. And the compiler can show an error if he mistyped the type of e.
Coordinator
Mar 12, 2014 at 7:25 PM
The function is almost always contextually typed, though. If you want the implied type of the parameter, you can just omit the type names on the arguments and get the type checking you want. In a sense, providing a parameter type on a contextually-typed function expression is a cast.
// New experimental browser feature: Smell events

interface OdorEvent extends Event {
    odor: string;
}

// Error: Event does not have 'odor'. Desired 'sound' behavior by default
window.addEventListener('smellDetected', (event) => { console.log(event.odor); })

// Supply the type of the event, OK. Acts as a probably-safe cast
window.addEventListener('smellDetected', (event: OdorEvent) => { console.log(event.odor); })

// -- Proposed behavior --
// I've written the differing type name the same number of times,
// but with a bunch of extra characters for no reason?
window.addEventListener('smellDetected', <(event: OdorEvent) => void>((event) => { console.log(event.odor); });

// Cast a bunch of times? Or have a surplus local? Why?
window.addEventListener('smellDetected', (event) => {
    console.log((<OdorEvent>event).odor);
    console.log((<OdorEvent>event).odor);
});
Mar 13, 2014 at 9:59 AM
Edited Mar 13, 2014 at 10:09 AM
I see where you're coming from, I just don't agree with this decision. The reason for a cast is exactly to make it look horrible, so that it screams ‘Look here, this is wrong!’. What you're doing is you're hiding the cast from the developer. They'd have to look at two classes to see whether they are casting or not, and there's nothing on the spot to indicate that they are.

Let's not forget that a property access does not generate a runtime error in JavaScript. Therefore event.odor may return undefined. Or, if there's another subclass of Event passed in that has an odor property with a non-string value, you'll get something weird. That is exactly the type of bug that will be hard to find when you're hiding the cast. By the way, the correct approach is not to cast multiple times, but to check the type with instanceof:
interface OdorEvent extends Event {
    odor: string;
}
// So we can use it with instanceof
declare var OdorEvent: {
    prototype: OdorEvent;
    new (): OdorEvent;
}

window.addEventListener('smellDetected', (event) => {
    if (event instanceof OdorEvent) {
        // PROPOSAL: The compiler should know at this point that event is an OdorEvent
        // This gives an error at the moment.
        // (The Closure Compiler does know it.)
        console.log(event.odor);
        console.log(event.odor);
    }
});
That is the exact same reason why I loathe not having an ‘override’ keyword for methods. Makes it difficult to find bugs.
Mar 13, 2014 at 10:20 AM
Of course, this could be more elegant, given the truly fantastic generic support in TS:
interface OdorEvent extends Event {
    odor: string;
}

function on<T extends Event>(t: string, c: (e: T) => void): void {}

on<OdorEvent>('smellDetected', (e) => {
    console.log(e.odor);
    console.log(e.odor);
});