Paulius Liekis
Paulius Liekis

Reputation: 1730

Why does Typescript allow slicing of types?

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?

Upvotes: 3

Views: 522

Answers (4)

Paulius Liekis
Paulius Liekis

Reputation: 1730

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.

Upvotes: 0

calios
calios

Reputation: 535

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.

Upvotes: 0

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250156

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.

Upvotes: 4

HTN
HTN

Reputation: 3604

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

Upvotes: 0

Related Questions