3

Can someone explain to me why does this compile in Typescript?

class Result<T> {
    object: T | null = null;
}

function setOnlyA(res: Result<{ a: number }>) {
    res.object = { a: 5 };
}

function setAB(res: Result<{ a: number; b: string }>) {
    setOnlyA(res);
    // everything compiles fine, but res object is invalid 
    // at this point according to type definitions
}

I would expect setOnlyA call to be disallowed in setAB. I have strict mode on. Do I need some other setting?

Paulius Liekis
  • 1,676
  • 3
  • 18
  • 26
  • 3
    As Titian notes, this was a choice, and it creates type problems, while making some things more convenient. As an interesting note, Swift made the opposite choice, and prevents those problems (while being less convenient), and a very common question is "why doesn't this compile in Swift?" https://stackoverflow.com/questions/30487258/swift-generics-upcasting/30487474#30487474 – Rob Napier May 13 '20 at 13:56

4 Answers4

4

This is a fundamental issue with the typescript type system unfortunately. Fields are assumed to be covariant, even though a readable and writable field should actually make the type invariant. (If you want to read about covariance and contravariance, see this answer).

Ryan Cavanaugh explains in this:

This is a fundamental problem with a covariant-by-default type system - the implicit assumption is that writes through supertype aliases are rare, which is true except for the cases where it isn't.

Being very strict about field variance would probably result in a great deal of pain for users, even enabling strict variance for functions was only done for function types and not for methods, as detailed here:

The stricter checking applies to all function types, except those originating in method or construcor declarations. Methods are excluded specifically to ensure generic classes and interfaces (such as Array)

There are proposals to enable writeonly modifiers (and be stricter about readonly) or have explicit co/contra-variant annotations, so we might get a strict flag at a later date, but at this time this is a unsoundess/usability tradeoff the TS team has made.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Thanks a lot! I'll try to solve my problem by passing some sort of a result-object setter function - it should be possible to make it complain. – Paulius Liekis May 13 '20 at 13:53
  • @PauliusLiekis that is actually simple to do since function parameter types (if you face strictFunctionsEabled) are contravariant: https://www.typescriptlang.org/play/?ssl=12&ssc=51&pln=10&pc=35#code/MYGwhgzhAEBKCmECuIAuAeAKgPmgbwFgAoaU6AewCMAreYVALmk2gB9oA7FEaAXk+4BuYgF9ixAGZIO9AJbkO0CPFQB5DiACeAQQAUy1AghNdABwZ5oYJlwC2leACdoIgJR9cAN3KyAJu8ISMgMjXUtraABWF1dRcSIpGVR5RQNtACFdR0QmIxQMcJskeydBaEomCFRHWQ4AcxdsAOIyJRV1LT1PD2hsiAA6Klp6PmhPV2EiESA – Titian Cernicova-Dragomir May 13 '20 at 13:55
0

It's OK because { a: number; b: string } is a subtype of { a: number}. That's how Typescript works: https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#14-structural-subtyping

HTN
  • 3,388
  • 1
  • 8
  • 18
  • Yes, but it doesn't make sense in this case as Typescript fails to ensure that runtime result matches type definition, i.e. member b is missing on res after a call to setOnlyA. – Paulius Liekis May 13 '20 at 13:50
0

the system is caring that you implements the type, not that its the exact same type, think about it like an interface -

  1. you have to implement its properties.
  2. you can add properties to the class that implemented it.
calios
  • 525
  • 5
  • 13
0

My fixed code looks something like this:

class Result<T> {
    private object: T | null = null;

    // this solves the problem
    setObject = (o: T) => {
        this.object = o;
    };

    // this doesn't
    //setObject(o: T) {     
    //  this.object = o;
    //};
}

function setOnlyA(res: Result<{ a: number }>) {
    res.setObject({ a: 5 });
}

function setAB(res: Result<{ a: number; b: string }>) {
    setOnlyA(res);
}

i.e. the solution is to use lambda as a setter. Using regular member function doesn't work - the typescript fails to discover the problem just as in the original code.

Paulius Liekis
  • 1,676
  • 3
  • 18
  • 26