Typescript type narrowed to never with instanceof in an if-else statement -


i have problem when try use instanceof derived class instances in if-else statement. consider following example:

interface ibasemodel {     id: string }  class baseclass {     model: ibasemodel     constructor() {     }      setmodel(model: ibasemodel) {         this.model = model     }      getvaluebyname(name: string) {         return this.model[name];     } }  interface iderived1model extends ibasemodel {     height: number; }  class derived1 extends baseclass {     setmodel(model: iderived1model) {         super.setmodel(model);         // model...     } }  interface iderived2model extends ibasemodel {     width: number; }  class derived2 extends baseclass {     setmodel(model: iderived2model) {         super.setmodel(model);         // model...     } }  const model1 = { id: "0", height: 42 }; const model2 = { id: "1", width: 24 };  const obj1 = new derived1(); obj1.setmodel(model1);  const obj2 = new derived2(); obj2.setmodel(model2);  const objs: baseclass[] = [     obj1,     obj2 ];  let variable: = null; (const obj of objs) {     if (obj instanceof derived1) {         variable = obj.getvaluebyname("height"); // ok, obj of type `derived1`     } else if (obj instanceof derived2) {         variable = obj.getvaluebyname("width"); // not compile: property 'getvaluebyname' not exist on type 'never'     }     console.log("value is: " + variable); } 

here, getvaluebyname cannot called on obj in else part, narrowed never. somehow, typescript thinks else never executed, wrong.

the important thing @ overriding of function setmodel. overrides have different parameter types, types inherit base ibasemodel type. if change base type, typescript doesn't complain , compiles fine :

class derived1 extends baseclass {     setmodel(model: ibasemodel) {         super.setmodel(model);         // model...     } }  class derived2 extends baseclass {     setmodel(model: ibasemodel) {         super.setmodel(model);         // model...     } } 

so question is, why having overrides different types make instanceof operator narrow type of object never? design?

this tested typescript 2.3.4, 2.4.1 , in typescript playground.

thanks!

welcome world of typescript issue #7271! you've been bitten typescript's structural typing , strange (and frankly unsound) interactions instanceof.

typescript sees derived1 , derived2 same type, because have same structural shape. if obj instanceof derived1 returns false, typescript compiler thinks both "okay, obj not derived1" , "okay, obj not derived2", since doesn't see difference between them. , when check obj instanceof derived2 returning true, compiler says "gee, obj both , not derived2. can never happen." of course there is difference between derived1 , derived2 @ runtime, , can happen. problem.

the solution: shove differing property derived1 , derived2 typescript can tell difference between them. example:

class derived1 extends baseclass {     type?: 'derived1'; // add line     setmodel(model: iderived1model) {         super.setmodel(model);         // model...     } }  class derived2 extends baseclass {     type?: 'derived2'; // add line     setmodel(model: iderived2model) {         super.setmodel(model);         // model...     } } 

there's optional type property on each class different string literal type (without changing emitted javascript). typescript realizes derived1 not same derived2 , error goes away.

hope helps. luck!


update 1

@sebastien-grenier said:

thanks explanation! however, fail see why typescript considers them structurally identical when types of parameter in override different, compiles fine when type identical (i.e. same parent, ibasemodel). also, happens if have member called type on object? can conflict type? ?. thanks!

wow, strange. looks there change (#10216) @ point fix instances of issue #7271, managed find new one. guess because override setmodel method narrower argument type (which unsound, way... every baseclass should have setmodel() accepts any ibasemodel. if you're interested in doing soundly can talk), fools code change in #10216 not applying. might bug... may want file it.

yes, if have property same key should pick new one. idea brand types. can pick name __typebrand if you're worried accidental conflict.

but there more straightforward change not conflict:

class derived1 extends baseclass {     model: iderived1model;     // overrides follow }  class derived2 extends baseclass {     model: iderived2model;     // overides follow } 

presumably want each class know model narrowed type, right? doing above narrowing of model both lets compiler know types structurally distinct , makes derived classes safer use.

cheers!


Comments

Popular posts from this blog

javascript - Create a stacked percentage column -

Optimising Firebase database by automatically overwriting data -

javascript - Angular UI-Grid customTemplate directive causing rows to load slowly/? -