Home > Software design >  Ignoring generic type argument when the specifity is not needed in Typescript
Ignoring generic type argument when the specifity is not needed in Typescript

Time:11-24

Suppose I have a class with an elaborate generic type argument like

class SomeStuff<T extends ElaborateStuff> {...}

Elsewhere I have a function with a SomeStuff parameter like

function doSimpleThings(stuff: SomeStuff<X>): void {...}

which is independent of the specific incarnation of X as an extension of ElaborateStuff. The above is complained about by the compiler, because X must extend ElaborateStuff. I could write

function doSimpleThings(stuff: SomeStuff<any>): void {...}

but then eslint complains about the explicit use of any. Is there another way than switching off the any warning? In Java I would use <?>, is there a similar syntax in Typescript?

CodePudding user response:

TL;DR: There is no ? syntax in typescript. It really depends on the variance of your generic type if you can use anything except any, but you probably will use any classes usually end up being invariant.

At it's core this is a question of variance. If you want to know more on that you can check out my talk here

If the class is co-varinat you can use ElaborateStuff and any instantiation of SomeStuff will be compatible with it as SomeStuff<ElaborateStuff> will be the base class of all instantiations:

type ElaborateStuff = {
    a: string
}
class SomeStuff<T extends ElaborateStuff> {
    getVStuff(): T {
        return null!;
    }
}

function doSimpleThingsElaborateStuff(stuff: SomeStuff<ElaborateStuff>): void { }


doSimpleThingsElaborateStuff(new SomeStuff<{ a: "", }>()); //OK
doSimpleThingsElaborateStuff(new SomeStuff<{ a: "", b: "",}>()); // OK

function doSimpleThingsNever(stuff: SomeStuff<never>): void { }
doSimpleThingsNever(new SomeStuff<{ a: "", }>()); // Error 
doSimpleThingsNever(new SomeStuff<{ a: "", b: "",}>()); // Error

Playground Link

If your class is contra-variant you can use never and any instantiation of SomeStuff will be compatible with it as SomeStuff<never> will be the base class of all instantiations:

type ElaborateStuff = {
    a: string
}
class SomeStuff<T extends ElaborateStuff> {
    getStuff = (v: T): void  => null!
}

function doSimpleThingsElaborateStuff(stuff: SomeStuff<ElaborateStuff>): void { }

doSimpleThingsElaborateStuff(new SomeStuff<{ a: "", }>()); // error
doSimpleThingsElaborateStuff(new SomeStuff<{ a: "", b: "",}>()); //error

function doSimpleThingsNever(stuff: SomeStuff<never>): void { }

doSimpleThingsNever(new SomeStuff<{ a: "", }>()); // ok 
doSimpleThingsNever(new SomeStuff<{ a: "", b: "",}>());// ok

Playground Link

If your type is invariant however, you can only use any. Lint rules are useful but can be ignored and there are good use cases for any, such as in this case. However you should be aware this is type not safe, so you should not rely on the compiler to check any operation in which the type parameter is involed:

type ElaborateStuff = {
    a: string
}
class SomeStuff<T extends ElaborateStuff> {
    getStuff = (v: T): T  => {
        v.a.big(); // error if a is not there
        return v;
    }
}

function doSimpleThingsElaborateStuff(stuff: SomeStuff<ElaborateStuff>): void { }

doSimpleThingsElaborateStuff(new SomeStuff<{ a: "", }>()); // error
doSimpleThingsElaborateStuff(new SomeStuff<{ a: "", b: "",}>()); //error

function doSimpleThingsNever(stuff: SomeStuff<never>): void { }

doSimpleThingsNever(new SomeStuff<{ a: "", }>()); // error
doSimpleThingsNever(new SomeStuff<{ a: "", b: "",}>());// error


function doSimpleThingsAny(stuff: SomeStuff<any>): void {
    stuff.getStuff({}) // will trigger an error
    stuff.getStuff({ a: ""}).foo // error on foo access
}

doSimpleThingsAny(new SomeStuff<{ a: "", }>()); // ok
doSimpleThingsAny(new SomeStuff<{ a: "", b: "",}>()); // ok

Playground Link

  • Related