lorefnon
lorefnon

Reputation: 13095

How to declaratively enforce compatibility of types?

I would like to enforce a (compile-time) constraint that a variable of type T2 be assignable to a variable of type T1.

If I have a value (t2) of type T2 then I can do the following:

const t1: T1 = t2;

Is there a better way to do this ? Preferably without having to create additional runtime entities.


I don't define either of these types directly in my application so I can not make one the superclass of other.

Upvotes: 2

Views: 178

Answers (2)

jcalz
jcalz

Reputation: 327934

Are you looking for a witness that will give you a compile error if T2 is not assignable to T1 without emitting anything at runtime? Sure, you can define something like this:

type ExtendsWitness<U extends T, T> = U

and use it like this:

type T2Witness = ExtendsWitness<T2, T1>

If T2 is assignable to T1, you will get no errors. Otherwise, you will get an error on the first type argument to ExtendsWitness saying that T2 does not meet the constraint T1.


Here's an example with some concrete types:

interface Super {
  foo: string;
  bar: number;
}

interface GoodSub {
  foo: 'a';
  bar: 3;
  baz: boolean;
}

type GoodWitness = ExtendsWitness<GoodSub, Super> // okay

Notice that GoodSub is not defined in terms of Super, but GoodSub is witnessed by the compiler to be a subtype of Super nonetheless. Here's an example of a bad subtype:

interface BadSub {
  foo: string;
  baz: boolean;
}

type BadWitness = ExtendsWitness<BadSub, Super> // error

The compiler cannot witness that BadSub is a subtype of Super, and the error tells you exactly what the problem is: Type 'BadSub' does not satisfy the constraint 'Super'. Property 'bar' is missing in type 'BadSub'.

Hope that helps; good luck!

Upvotes: 1

smnbbrv
smnbbrv

Reputation: 24541

I don't think the thing you want is achievable as is. Sorry for the stupid sentence, but as long as you cannot define the relativity of the types => you cannot define relativity of the types.

However, you still have the following options:

  1. Use any type to simply assign the values

    const t1: T1 = <any>t2;
    const t3: T2 = <any>t1;`
    
  2. Similar solution with a caster function:

    function toT1(t: T2): T1 {
        return <any>t;
    } 
    
    const t1 = toT1(t2);
    

    Advantage is that you don't need to define t1 type.

  3. Create a type that merges two types into one

    type T3 = T1 | T2;
    
    const t1: T3 = t2;
    

    This solves the problem with assignment but could lead to some problems with the entities that are already bound to those T1 and T2. This can be used situationally depending on the needs

  4. Create a custom type guard:

    interface T1 { a: any; }
    
    interface T2 { b: any; }
    
    function isT1(t: T1 | T2): t is T1 {
        return true;
    }
    
    const t2: T2 = { b: 1 };
    const t1: T1 = isT1(t2) ? t2 : null;
    

    This returns true all the time so you can assign your variable to t1 without any problem. However this is still ugly.

I would go with a way number 1. It is simple and fully covers your needs. Over engineering is also bad.

Upvotes: 0

Related Questions