Skip to content

Type safety lost when generic type used as method parameter #57071

Closed as not planned

Description

@TheChilliPL

🔎 Search Terms

Type safety lost when generic type used as method parameter

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about generic type safety

⏯ Playground Link

https://www.typescriptlang.org/play?#code/MYGwhgzhAECC0G8BQ1XTACgJSJW-wA9gHYSEgCmAdCIQOYYBEY0AlsQG6EDWFAJoywBuPKgC+SCaEgwAQtAoAPAC4VifGPGT4ARtlz4CJMpRr0mOtpx79BI-BIlJpUaAFkAngGFwUADwAKgB8BmhEpMoATgCuwMqEkRgcYCDRFABc0AE42oaoygAWrBBUyakU0AC80GVp9mhO+AAOkazJqjUpaZkBIqLQdBTKAGpdFNg9oXmRQ9GRxNCFxaVj9eJI-RBDo+VJYz1YmVysfFOGSyW1FdVXa9COG5TK0Do+MlXQxBQA7u7evhAMF9frJsMIkK8AVQtiMxkCftBQVhwUgnug3lBMp4MRA-LAQtVITIRGAcdDtnDgXAwX0iVAqINYbssFQ9MIgA

💻 Code

class A {
    a() {
        console.log("a invoked");
    }
}
class B extends A {
    b() {
        console.log("b invoked");
    }
}

class MyClass<T> {
    constructor(value: T) {
        this.value = value;
    }

    private value: T;

    getValue(): T {
        return this.value;
    }

    setValue(value: T): void {
        this.value = value;
    }
}

let bClass = new MyClass(new B());
bClass.setValue(new B());

let aClass: MyClass<A> = bClass;
aClass.setValue(new A());

bClass.getValue().b();

🙁 Actual behavior

TypeScript allowed setting to a supertype, thus invalidating type safety

[ERR]: "Executed JavaScript Failed:" 
[ERR]: bClass.getValue(...).b is not a function 

🙂 Expected behavior

Possibly don't allow casting generics to their supertypes if the class contains methods containing parameters of generic type?

Additional information about the issue

No response

Activity

  1. fatcerberus commented on Jan 16, 2024

    @fatcerberus

    Methods are intentionally bivariant so this is the same mode of unsoundness as

    class A {
        a() {
            console.log("a invoked");
        }
    }
    class B extends A {
        b() {
            console.log("b invoked");
        }
    }
    
    let bList = [ new B() ];
    bList[0] = new B();
    
    let aList: A[] = bList;
    aList[0] = new A();
    
    bList[0].b();  // runtime error

    which is not considered to be a defect.

  2. fatcerberus commented on Jan 16, 2024

    @fatcerberus

    The relevant FAQ entry btw is

    • A method and a function property of the same type behave differently.
      • Methods are always bivariant in their argument, while function properties are contravariant in their argument under strictFunctionTypes. More discussion here.
  3. TheChilliPL commented on Jan 16, 2024

    @TheChilliPL
    Author

    Still, the strictFunctionTypes option excludes functions created as method declarations, thus making this example still compile with this option turned on.
    That being said, yes, introducing explicit type-safe covariant and contravariant generics would require major changes to existing type definitions, but many (strict-typed) languages manage to do that with keywords like in and out (C# or Kotlin). It'd be also possible to introduce something like wildcard types in Java, where instead you have to specify the variance when using the types by using ? extends T for covariance and ? super T for contravariance

  4. fatcerberus commented on Jan 16, 2024

    @fatcerberus

    Still, the strictFunctionTypes option excludes functions created as method declarations, thus making this example still compile with this option turned on.

    Yes, that’s specifically called out in the FAQ excerpt I quoted. From the PR linked there, that was an intentional trade-off:

    Methods are excluded specifically to ensure generic classes and interfaces (such as Array<T>) continue to mostly relate covariantly. The impact of strictly checking methods would be a much bigger breaking change as a large number of generic types would become invariant (even so, we may continue to explore this stricter mode).

    TypeScript actually does have variance annotations, but they’re only honored in nominal type checks (i.e. between two instantiations of the same generic), and have no effect on structural comparisons.

  5. craigphicks commented on Jan 16, 2024

    @craigphicks

    Chilli! (@TheChilliPL)

    Even if that were illegal - you would still want to be able to pass MyClass<B> via a parameter Readonly<MyClass<A>> for reading purposes. However in

    function doSomethingWithALikeness(a:Readonly<MyClass<A>>){
        a.setValue(new B())
    } 

    Readonly is not strict enough to prevent a.setValue(new B()). Something like DeepReadonly, DeepFrozen, or maybe a pure keyword on the function is required.

    I.e., this would be nice

    let aClass: DeepReadonly<MyClass<A>> = bClass;
    aClass.setValue(new A()); // error, mutations not only allowed under DeepReadonly 
  6. TheChilliPL commented on Jan 17, 2024

    @TheChilliPL
    Author

    Craig P Hicks (@craigphicks) I don't think Readonly is really for that. As docs say, “[Readonly] constructs a type with all properties of Type set to readonly, meaning the properties of the constructed type cannot be reassigned”, so it only makes all the properties readonly. Making a method inaccessible wouldn't really make sense.

  7. TheChilliPL commented on Jan 17, 2024

    @TheChilliPL
    Author

    Bruce Pascoe (@fatcerberus) Nice to see Typescript actually has variance annotations, didn't find anything about it before. Shame the default is bivariant and that there's no way to enable strict method variance checks, as far as I know. It would be useful to ensure full type safety of generics.

    Also, I think it's not possible to use them as a variable type? Like this:

    interface Box<in out T> {
    	set(value: T): void;
    	get(): T;
    }
    
    let inBox: Box<in T> = …;
    let outBox: Box<out T> = …;
    
    inBox.set(…);  // Fine
    outBox.get();  // Fine
    
    inBox.get();   // Error
    outBox.set(…); // Error

    It could work a bit like wildcard generics in Java, by locking the ability to perform certain things that could break type safety. Unless it would be possible using some utility type? TypeScript allows lots of magic with it, but can't think of a way that would make that possible

  8. TheChilliPL commented on Jan 17, 2024

    @TheChilliPL
    Author

    Also, from what I can see, there isn't a way to explicitly state bivariance in a way that would make the compiler ensure the type is both covariant and contravariant?

  9. craigphicks commented on Jan 17, 2024

    @craigphicks

    Chilli! (@TheChilliPL)

    Craig P Hicks (@craigphicks) I don't think Readonly is really for that. As docs say, “[Readonly] constructs a type with all properties of Type set to readonly, meaning the properties of the constructed type cannot be reassigned”, so it only makes all the properties readonly. Making a method inaccessible wouldn't really make sense.

    Exactly - Readonly is really [not] for that., that's my point.

    In the case you only want to read the A-ness of b, you should have a way to do so (and TypeScript currently offers that, as shown in your example) . It would be nice to have a way to read the A-ness of b without risking the kind of error you demonstrate in your example.

    However, TypeScript can't make your example an error, because TypeScript puts priority on allowing reading the A-ness of B, and TypeScript has no way to prevent a.setValue being called.

    If you proposed a way to prevent a.setValue being called - say

    let aClass: DeeplyImmutable<MyClass<A>> = bClass;
    

    (and also features like declaring Methods and Parameters as mutating or not) then TypeScript could make

    let aClass: MyClass<A> = bClass;
    

    an error because there would be an alternative safe way (DeeplyImmutable) to allow reading the A-ness of b.

  10. fatcerberus commented on Jan 17, 2024

    @fatcerberus

    Shame the default is bivariant

    The default is actually "let the compiler measure the variance automatically". Explicit variance annotations were eventually added because it can sometimes get it wrong. When it's working properly, the measured variance is supposed to match the structural variance. In this case, the type is "correctly" measured as covariant because the type parameter T doesn't appear in any purely contravariant positions (thanks to the method exception).

    there isn't a way to explicitly state bivariance

    Does it ever actually make sense for a type parameter to be bivariant, assuming it's actually used somewhere in the type?

  11. ritschwumm commented on Jan 17, 2024

    @ritschwumm

    Bruce Pascoe (@fatcerberus) i guess phantom types are the only thing where bivariance makes any sense...

  12. TheChilliPL commented on Jan 17, 2024

    @TheChilliPL
    Author

    Does it ever actually make sense for a type parameter to be bivariant, assuming it's actually used somewhere in the type?

    That's fair, actually; but at the same time it represents how default bivariance inherently implies type safety problems in most cases—that's why I wish we could make it stricter.

    Even --strictFunctionTypes doesn't seem to work as expected:

    • set(value: T): void; seems to be completely allowed in a type with <out T>
    • You can assign Type<T> to both Type<Supertype> and Type<Subtype> even when T is covariant or contravariant, without even a compiler warning

    See playground

  13. fatcerberus commented on Jan 17, 2024

    @fatcerberus

    default bivariance

    As I said, variance is measured - generics are not “bivariant by default” in any meaningful sense. Method parameters are bivariant with respect to assignability of the overall type—which can influence variance measurement, of course.

    set(value: T): void; seems to be completely allowed in a type with <out T>

    That’s expected - without the annotation the type would be measured as covariant anyway, so the annotation adds no information.

    You can assign Type<T> to both Type<Supertype> and Type<Subtype> even when T is covariant or contravariant, without even a compiler warning

    Actually that’s just because all of those are empty types. Make them structurally different and you’ll find the variance annotations work as expected. This is just a reality of structural typing. See https://github.com/microsoft/TypeScript/wiki/FAQ#why-do-these-empty-classes-behave-strangely

  14. TheChilliPL commented on Jan 18, 2024

    @TheChilliPL
    Author

    That’s expected - without the annotation the type would be measured as covariant anyway, so the annotation adds no information.
    But that means you could still do

    let generic: OutGeneric<Class> = …;
    let generic2: OutGeneric<Supertype> = generic;

    breaking type-safety again, despite --strictFunctionTypes being on—see Playground

  15. fatcerberus commented on Jan 18, 2024

    @fatcerberus

    Yes, you've annotated it covariant (out), and TS doesn't object because that's what it would measure anyway. The type is properly invariant (in out).

  16. TheChilliPL commented on Jan 20, 2024

    @TheChilliPL
    Author

    Could I make TypeScript then disallow using a covariant generic in method parameters?

  17. fatcerberus commented on Jan 20, 2024

    @fatcerberus

    That's more or less just strictMethodTypes and it's already been explained above why that doesn't exist

  18. typescript-bot commented on Jan 23, 2024

    @typescript-bot
    Contributor

    This issue has been marked as "Question" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  19. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions