Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bad function assignation

I get a problem in non strict mode, Here is my code I can assign a function with bad type to a typed variable.

Do I make a mistake ?

Thanks.

interface A {
    f1( ) : void;
}

interface B extends A {
    f2( ) : void;
}

interface C {
    f3( ) : void;
}

type expectA = ( a: A ) => void;
type expectB = ( b: B ) => void;


function testA( a: A ) {
    console.log( "a" );
}

function testB( b: B ) {
    console.log( "b" );
}

function testC( c: C ) {
    console.log( "c" );
}


let v1: expectA = testA;    // ok: assign type A to a type A
let v2: expectA = testB;    // ok: assign type B (extending A) to a type A
let v3: expectA = testC;    // fail -> normal: error TS2322: Type '(c: C) => void' is not assignable to type 'expectA'.
let v4: expectB = testA;    // ok -> **abnormal**: there is no error in !strict mode
like image 877
eco747 Avatar asked Nov 07 '22 02:11

eco747


1 Answers

TypeScript is actually protecting you against doing something silly here, but it's doing so in a confusing way.

Let's reduce your example to just the interfaces A and B.

interface A {
    f1( ) : void;
}

interface B extends A {
    f2( ) : void;
}

type expectA = ( a: A ) => void;
type expectB = ( b: B ) => void;


function testA( a: A ) {
    a.f1();
}

function testB( b: B ) {
    b.f1();
    b.f2();
}


const v1: expectA = testA;
const v2: expectA = testB; // error: Property 'f2' is missing in type 'A' but required in type 'B'
const v3: expectB = testB;
const v4: expectB = testA;

On first glance, the result at the bottom where only v2 has an error might seem counterintuitive. If B extends A, then why can't you use B everywhere you can use A?

The answer is because we're dealing with functions here. Look closely at the implementation of testB(). It calls the property b.f2() because it expects that its parameter b will have that property. But the left hand side of const v2: expectB is equivalent to the type (a: A) => void. The parameter of type A does not have f2(). So we're telling TypeScript conflicting things about what the proper type of v2 is; either it's a function that has a: A in which case a.f2() is not safe to call, or it's a function that has b: B in which case it is safe. It's a paradox!

(Keep in mind that this has nothing to do with whether testB actually tries to call b.f2(); the point is that it could, based on how its argument types are set, which would cause a runtime error in the v2 scenario.)

Now for const v4 you say you think it's "abnormal" that this would be OK, but if we look carefully at the functions again we can see that it makes sense that it is OK. If you passed either a type A or B variable to testA(), there are no possible errors because it will never try to access the f2() property.

Note also that extends does not work in TypeScript quite the way you might expect it to. Writing interface B extends A simply says that B will inherit all the properties of A. It does not establish any kind of relationship allowing B to stand in for any instance of A. Such behavior is called "polymorphism" and to do that you would need to use classes instead, e.g. class B extends A implements A.

class A {
    foo = '';
}

class B extends A implements A {
    bar = '';
}

let isA = new A();
isA = new B(); // this is fine because B implements A
let isB = new B();
isB = new A(); // error: Property 'bar' is missing in type 'A' but required in type 'B'
like image 84
jered Avatar answered Nov 27 '22 05:11

jered