typescripttype-inferencestructural-typing

When does TypeScript use structural typing?


Given the types below why does the compiler allow the assignment below? I'm guessing it has to do with TypeScript using structural typing in some situations (i.e. since Success and Failure are structurally equivalent the compiler treats them as interchangeable), but I guess I'm not clear under what conditions is structural typing used.

////////////////////
// Types
////////////////////

class Success<S> {
    constructor(public value: S) { }
    static create<S, F>(value: S):Result<S, F> {
        return new Success<S>(value);
    }
}

class Failure<F> {
    constructor(public value: F) {}
    static create<S, F>(value: F): Result<S, F> {
        return new Failure<F>(value);
    }
}

type Result<S, F> = Success<S> | Failure<F>;


////////////////////
// Usage
////////////////////

/* 
How is the assignment below allowed by the compiler? 
Failure.create produces a Result<unknown, number> which should not
be assignable to a Result<number, Error>
*/

const f: Result<number, Error> = Failure.create(2);

EXTRA CONTEXT FOR THE CURIOUS: As Malvolio points out the issue is that type unions are commutative and the way to get around that is to have the types not be structurally equivalent. Malavolio's solution is to give the types different fields (i.e. svalue and fvalue). This works but I prefer to have the interface be the same so I opted for the solution below using symbols to differentiate the classes (If this has issues associated with it please chime in):

export const SuccessType = Symbol();
export class Success<S> {
    public readonly resultType = SuccessType;
    private constructor(public value: S) {}
    static create<S, F>(value: S): Result<S, F> {
        return new Success<S>(value);
    }
}

export const FailureType = Symbol();
export class Failure<F> {
    public readonly resultType = FailureType;
    private constructor(public value: F) {}
    static create<S, F>(value: F): Result<S, F> {
        return new Failure<F>(value);
    }
}

Solution

  • Wow. It is very subtle, but try this instead:

    class Success<S> {
        constructor(public svalue: S) { }
        static create<S, F>(value: S):Result<S, F> {
            return new Success<S>(value);
        }
    }
    
    class Failure<F> {
        constructor(public fvalue: F) {}
        static create<S, F>(value: F): Result<S, F> {
            return new Failure<F>(value);
        }
    }
    
    type Result<S, F> = Success<S> | Failure<F>;
    
    
    const f: Result<number, Error> = Failure.create(2);
    

    Your Result<S, F> is, by definition, anything that has a property value with the type S | F. Consequently, Result<S, F> is assignable to Result<F, S> — type-union is commutative.

    In my version, Result<S, F> is something that has either a property svalue with the type S or a property fvalue with the type F, so Result<S, F> is not assignable to Result<F, S>