Property constrained to two possible types?

Topics: General
Oct 3, 2013 at 4:13 PM
Edited Oct 3, 2013 at 4:14 PM
Is it possible to specify that a property on an interface can be one of two different types? For example, I have code that understands how to work with X if it is either a number or a boolean. Is there any way to specify that? I know I can use the any type, but it's technically not quite right, so I wanted to see if there was a better way.
Developer
Oct 4, 2013 at 1:39 AM
No that isn't possible, TypeScript does not have any concept of union types at present.
Oct 6, 2013 at 10:04 PM
Edited Oct 7, 2013 at 5:09 PM
While TypeScript doesn't have union types, you can help the type system by modeling the problem as tagged unions/sums. Somewhat like:
// two functions to map an L or an R to a T
interface Case<L,R,T> {
    left : (l:L)=>T;
    right: (r:R)=>T
}
// one function to tag L as left and R as right
interface Inject<L,R> {
    (l:L):Sum<L,R>;
    (r:R):Sum<L,R>
}
// tagged sum type: either an L, tagged left, or an R, tagged right
// (can't model "exactly one" - that is only enforced by construction)
interface Sum<L,R> {
    left?: L;
    right?: R
}
// inject boolean or number into Sum<boolean,number>
var BNin: Inject<boolean,number> = 
    ((bn)=>{
        if (typeof bn=="boolean")
            return <Sum<boolean,number>>{left:bn};
        else if (typeof bn=="number")
            return <Sum<boolean,number>>{right:bn};
        else
            throw ("BNin "+typeof bn)});
// combine Case with Sum to get result
function out<L,R,T>(caseof:Case<L,R,T>) {
    return (bn:Sum<L,R>) =>
            bn.hasOwnProperty("left")
            ? caseof.left(bn.left)
            : caseof.right(bn.right)
}
// convert boolean or number to string
var BNtoString : Case<boolean,number,string> = 
    {left: b=>b.toString()
    ,right: n=>n.toString()};

var BN : Sum<boolean,number> = BNin(true);

console.log( out(BNtoString)(BN) );
[BNin(false),BNin(42)]
    .map(out(BNtoString))
    .forEach( el=>console.log(el) )
Sadly, this duplicates JS' internal runtime type tagging, but by doing this, it makes the types available to the TypeScript type system.
Oct 10, 2013 at 9:39 AM
Actually, we can do better than that, for the specific case of boolean or number, by using an encoding of type Dynamic and type case that makes use of JS runtime type information (avoiding the extra level of tagging). Then we drop all the cases that don't deal with boolean or number.
// -------------------- start encoding of union type as restricted Dynamic/any

// a function for every dynamic type
interface TypeCase<T> {
    number : (n:number)=>T;
    boolean: (b:boolean)=>T;
}

// tell the TS type system which types can go into any
interface ToDynamic {
    (n:number):any;
    (b:boolean):any;
}
var toDynamic : ToDynamic = (t=>t);

// combine TypeCase<T> with any to get result T
function fromDynamic<T>(caseof:TypeCase<T>) {
    return (t:any) => {
        switch (typeof t) {
            case "number": return caseof.number(<number>t);
            case "boolean": return caseof.boolean(<boolean>t);
            default: throw ("out "+typeof t);
    }
    }
}
// -------------------- end encoding of union type as restricted Dynamic/any

// application code example:

// convert any value to string, via type case
// (omitting a case here is a static error)
var typeCaseAnyToString : TypeCase<string> = 
    {number: n=>"a number: " + n.toString()
    ,boolean: b=>"a boolean: " + b.toString()
    };

// apply typeCaseAnyToString to some dynamically typed values
// (injecting a type excluded in ToDynamic is a static error
//  here, provided that Object is also excluded there)
[toDynamic(undefined)   // can't rule out undefined
,toDynamic(null)        // can't rule out null, either:-(
,toDynamic(false)
,toDynamic(42)
// ,toDynamic("hi") // type error
// ,toDynamic(x=>x) // type error
// ,toDynamic({})   // type error
].map(fromDynamic(typeCaseAnyToString))
 .forEach( el=>console.log(el) );
In case the concepts are unfamiliar: a value of type Dynamic is a value paired with a runtime representation of its static type, so toDynamic marks a transition from static to dynamic typing, and fromDynamic marks a transition from dynamic to static typing. We use any as our Dynamic, because JS already has runtime type information (that info is a lot less precise than TS static types, but for boolean and number that makes no difference).

toDynamic uses overloads to list the types we want to allow in our Dynamic type, fromDynamic uses an object with optional fields to list types we expect to find in our Dynamic type (providing a statically typed handler for each dynamically possible type - a typecase construct).

That seems to be fairly close to a union type for boolean an number, with one exeption: TS' type system does not allow us to rule out null and undefined. For undefined (no value), that is expected, but for null (no object), that is unexpected and sad.

I would be interested in comments from the TypeScript team on the permissive typing wrt null here.
Oct 10, 2013 at 10:50 AM
In a couple of d.ts files I've ended up declaring a base interface, then deriving versions with the alternative types, and declaring methods with an overload for each derived type.

For example, I was working with a method with an options object where either template or templateUrl are required, so:
interface ModalOptionsBase { controller?: string; }
interface ModalOptionsTemplate extends ModalOptionsBase { template: string; }
interface ModalOptionsTemplateUrl extends ModalOptionsBase { templateUrl: string; }

interface ModalService { open(options: ModalOptionsTemplate): Modal; open(options: ModalOptionsTemplateUrl): Modal; }
Would something like that work for your use case?
Coordinator
Oct 11, 2013 at 4:35 PM
Since we're all throwing in ways of doing this, let me throw in my 2 cents.

One straightforward way of modeling this is not on the type but on the contract using overloads. For example:
function takeNumberOrString(x: number): number;
function takeNumberOrString(x: string): string;
function takeNumberOrString(x: any) { return x; }
Now you have a function which when called with something that isn't a number or string will error. It doesn't give you union types in the type system, but it does give you some flexibility on the callee side.
Nov 23, 2013 at 12:06 AM
I think that sort of misses the point though.
In JavaScript, a property can be assigned a number of different values, and it's difficult right now to relate that in TypeScript.
This is particularly problematic when you are creating definition files for existing JS libraries.

For example, let's say that an existing JS library allows the following:
interface Ellipse {
    radius: <some type>;
    ... other members here ...
}
If you look at the JS docs for Ellipse, it accepts
  • a single number ... var x = <Ellipse>{ radius: 10 }; // x and y are both 10
  • a numerical array (with 2 entries) ... var x = <Ellipse>{ radius: [10, 5] }; // x = 10, y = 5
  • an object with x and y properties... var x= <Ellipse>{ radius: {x:10, y:5} }
Using "any" as the type is lame because it really can't be "any" type. It can only be a single number, a numerical array, or an object with x and y properties.
I can't change the signature of the JS library because it's not mine (and also, why would should I even if it was? it's perfectly valid the way it is), but i somehow need to represent this in the TS file, but can't.

In C++, sure, this would be a union, but in JS it's a simpler concept - it's a named bucket that accepts one value of three possible types - not ANY type. I just need a simple way to relate that in TS. I think creating unions in TS is overkill, but having a way to specify "one of x possible types" should be doable.
Nov 25, 2013 at 1:30 PM
@RobTeixeira, that's not a bad example. There is nothing in common between number, number[] and { x: number; y: number; }, so we can't model via inheritance a la @markrendle above.

You could try generics:
interface Ellipse<T> {
    radius: T;
}

function foo<T>(ellipse: Ellipse<T>) {
}

var ellipse1: Ellipse<number> = { radius: 10 }
var ellipse2: Ellipse<number[]> = { radius: [10, 11] }
var ellipse3: Ellipse<{ x: number; y: number }> = { radius: { x: 10, y: 11 } }

foo(ellipse1);
foo(ellipse2);
foo(ellipse3);
This permits a more explicit declaration than using any for radius, but since we can't define a generic constraint it's also possible to write
var ellipse4: Ellipse<string> = { radius: "10" }
Perhaps all we need is
interface Ellipse<T extends number|number[]|{x:number; y:number}> {
    radius: T;
}
Nov 25, 2013 at 5:41 PM
Your last example with the generic type constraints is a lot closer to what I think we need, except that's also not quite there.

The reason is that I might assign a value of one type to the radius property, call a bunch of methods, and then re-assign a new value (of a different type) to the radius property. Also, the return of the property might not be the same as what I initially set. A generic type constraint assumes that the value will always be the same type for the life of the instance, which in this case is clearly not true. This is all perfectly legal in JS, but is nearly impossible to describe in a TS definition file without opening it up to "any".

I don't know what it would break or what difficulties there would be for the compiler, but it would be nice if i can specify a set of legal types directly on the property itself (at least in ambient space inside a definition file).
Mar 27 at 5:02 PM
Hi,
I wanted to hijack this thread a little, in the direction of "creating definition files for existing JS libraries" that Rob referred to.

I am facing this with many .d.ts files I am trying to use - always having to change from one type to another (or any - which is lame). Many existing javascript libraries allow multiple value types on a field. Most commonly, it can be both string reference to variable and object reference - many mvc frameworks can only watch a variable by name, not by reference.

I came here to look for a way to map properly, instead of just changing existing mapping. So far, the only suggestion that I see is creating base interface and then overriding it as needed with different types. Problem is, if you have multiple fields allowing multiple types, it explodes the number of interfaces, you almost have single-use interfaces, which defeats the purpose.

What i would love to have (if we can't have union types), is ability to map multiple TS fields to same js variable, like so:
fieldByRef: IFieldType
fieldByName: string

in the compiled JS both would assign to same JS variable, but in TS I would like to have them separate. Current implementation of interfaces has no mapping logic whatsoever, and this would add some without breaking TS type system.