Typescript typings, generics and abstract classes

筅森魡賤 提交于 2019-12-02 11:27:21

问题


I experiment a behavior that seems strange to me.

Let's consider the following sample (test it in Typescript playground):

abstract class FooAbstract {
    abstract bar() {}
}

class Foo extends FooAbstract { 
    bar() { 
        return { bar: 'bar' };
    }
}

class FooMaker<FOO extends FooAbstract> {  
    constructor(public foo: FOO) {}

    bar() { 
        return this.foo.bar();
    }

    baz = () => { 
        return this.foo.bar();
    }
}

let foo = new Foo();
let result = foo.bar();

let foomaker = new FooMaker(new Foo);
let foo2 = foomaker.foo; // Type "Foo", OK
let result1 = foomaker.foo.bar(); // Type "{bar: string}", OK
let result2 = foomaker.bar(); // Type "{}", ???
let result3 = foomaker.baz(); // I've seen comments about using a lambda... Not better

result2 and result3 are typed like the abstract bar function ({}). It seems that this isn't resolved as the concrete class Foo but as the abstract class FooAbstract. Whereas the type of foo2 shows that the class foo property is resolved correctly.

What is going on? Do I do something the wrong way?


update

As an afterthought, this case can be reformulated like that (Test it in Typescript playground):

class Foo {
    bar() {
        return { bar: 'bar' };
    }

    getThis(): this {
        return this
    }
}

class Wrapper {  
    bar<FOO extends { bar(): {} }>(foo:FOO) {
        return foo.bar();
    }
}

let wrapper = new Wrapper();
let result = (new Foo()).bar();
let result2 = wrapper.bar(new Foo());

result has the type {bar:string}
result2 has the type {} (from the interface).
wrapper.bar has the type Wrapper.bar<Foo>(foo: Foo): {}

With this sample, it's clearer that, even when knowing that FOO is typed as Foo, Typescript uses FOO definition and not its explicit type as bar return type.


update 2

Ok, while fighting with typings, I think I leveled up. The concept is indeed that implicit typings in Typescript don't follow any inheritance model even when a type is deduced. Well, I still wonder why or is it going to change, but I'll have to cope with "it's like that". So in this case the type has to be explicit.

I found a simpler way to write his example (try it in Typescript playground):

abstract class FooAbstract {
    abstract bar(): {}
}

class Foo extends FooAbstract { 
    bar() { 
        return { bar: 'bar' };
    }
}

class FooMaker<FOO extends FooAbstract, BAR> {  
    constructor(public foo: FOO & { bar: () => BAR } ) {       
    }

    bar():BAR { 
        return this.foo.bar() as BAR;
    }
}

let foomaker = new FooMaker(new Foo());
let result = foomaker.bar();

result gets the type {bar:string} and no need to put generics everywhere. The stuff in the FooMaker.constructor parameter type could get cleaner by referring an interface with a generic.


回答1:


This is all about how the type resolution works for the bar function:

bar() { 
    return this.foo.bar();
}

What is this.foo? FOO or more precisely, a class that extends FooAbstract, because unlike the property foo, bar doesn't expose FOO. The typing has to be determined before the actual type FOO is defined.

You would have to do something like this, if you really wanted to type it:

abstract class FooAbstract<T extends {}> {
    abstract bar(): T
}

class Foo extends FooAbstract<{ bar: string }> { 
    bar() { 
        return { bar: 'bar' };
    }
}

class FooMaker<FOO extends FooAbstract<BAR>, BAR> {  
    constructor(public foo: FOO) {}

    bar():BAR { 
        return this.foo.bar();
    }

    baz = (): BAR => {
        return this.foo.bar();
    }
}

let foo = new Foo();
let result = foo.bar();

let foomaker = new FooMaker<Foo, { bar: string}>(new Foo);
let foo2 = foomaker.foo; // Type "Foo", OK
let result1 = foomaker.foo.bar(); // Type "{bar: string}", OK
let result2 = foomaker.bar(); // Type "{bar: string}", OK
let result3 = foomaker.baz(); // Type "{bar: string}", OK

And unfortunetly, you have to explicitly define the type of FooMaker, but you do prevent something like this:

let foomaker = new FooMaker<Foo, { bar: number}>(new Foo);



回答2:


Here is a clean answer and example of what is needed to pass method return types.

the issue

An object that embeds another object uses its internally declared type (in this case the abstract type) to determine its functions return type. Even when that object type is known (or explicitly declared).

In other words, Typescript type inference doesn't look inside the object methods to deduce a type.

the solution

The only solution I found to handle that case is to associate generics to the methods/functions return types, and to match the object structure with them.

Based on my question update 2 (test it in Typescript playground):

interface TestInterface<ASNUM, ASSTRING, ASOBJECT> {
    asNum: () => ASNUM
    asString: () => ASSTRING
    asObject: () => ASOBJECT
}

interface BaseInterface extends TestInterface<any, any, any> { }

class Obj implements BaseInterface {
    constructor(private n: number) { 
    }

    asNum() {
        return this.n;
    }

    asString() {
        return this.n.toString();       
    }

    asObject() { 
        return {value: this.n};
    }
}

class Wrapper<T extends BaseInterface, ASNUM, ASSTRING, ASOBJECT> {
    constructor(private obj: T & TestInterface<ASNUM, ASSTRING, ASOBJECT>) {
    }

    asNum() {
        return this.obj.asNum() as ASNUM;
    }

    asString() {
        return this.obj.asString() as ASSTRING;
    }

    asObject() {
        return this.obj.asObject() as ASOBJECT;
    }
}

let w = new Wrapper(new Obj(5));
let myNum = w.asNum();       // type: number
let myString = w.asString(); // type: string
let myObject = w.asObject(); // type: {value: number}

The types are OK!

alternatives

I didn't find a lot of things about that or that could help in the docs/upcoming features of Typescript 2.3. Concerning the things that could possibly help to shape a better solution:

  • There is a post about variadic types here, maybe this could help to improve such a sample (not sure though): https://github.com/Microsoft/TypeScript/issues/5453
  • Concerning the this references, there's a mention about strongly typing this here when using the --noImplicitThis compilation option, and the ThisType<T> function to declare this explicitely. But apparently it's more about a function being aware of its embedding structure type, than following the object model flow. And it doesn't help in my case.


来源:https://stackoverflow.com/questions/43485022/typescript-typings-generics-and-abstract-classes

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!