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);
}
}
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>