Typescript type compatibility test
Asked Answered
S

2

6

Does Typescript support a direct test for structural type compatibility?

C# supports the is operator and types surface IsAssignableFrom(object instance)

if (foo is SomeType) ...
if (SomeType.IsAssignableFrom(foo)) ...

Is there some direct way to perform this kind of check in Typescript or do I have to probe for each of the required members?

instanceof will probably do for the case at hand but does not honour structural compatibility, instead checking the prototype chain.

Yes yes, I know, Typescript's instanceof is directly equivalent to C#'s is operator. But I did start by specifying structural typing.

Maybe I should put a quack method on Object

Spittoon answered 17/5, 2016 at 4:9 Comment(0)
B
3

I believe the appropriate answer here is

No, but!

So just to get this out of the way, Typescript's type information exists only at compile time. There is no way to do a check of structural equality at runtime. You can do this check at compile time of course but that's just called assignment and probably not what you're asking about.

Long story short you have to probe for the required members (typically) but there are a few ways to be clever about this.


As of Typescript 1.6 the language supports user-defined type guards

These are wonderful as convenience functions and are a way of safely downcasting/specifying/refining a type (depending on your preferred terminology). You can manually check for the presence of certain types if you wish in the typical ways (.hasOwnProperty and such), in my experience they're more effective as ways to dispatch on message types. In most message passing systems you might have a type property. You can define a function

function isConfigGetMsg(msg: BaseMessage): msg is ConfigGetMsg {
  return msg.name === MsgTypes.CONFIG_GET_MSG;
}

//and use it as
if (isConfigGetMsg(msg)){
    console.log(msg.key); //key being a ConfigGetMsg only value
}

and this will be just fine as long as your messages are well-formed. Though as you can see you'll need to test for specific information. If you'd like a more general approach this aint it.


As you mentioned you can probe each of the required members. You can make it a bit easier for yourself by using the previous technique to create something like this:

interface One {
    a: number
}
interface Two extends One {
    b: string
}
function isAssignableTo<T>(potentialObj:Object, target:T) : potentialObj is T {
    return Object.keys(target).every(key => potentialObj.hasOwnProperty(key))
}

let a: Object = {
    a: 4,
    b: 'hello'
};

let b: Two = undefined;

if(isAssignableTo(a, b)) {
    b = a;
}

the isAssignableTo function being the key point here. The direction the parameters should be in is debatable but also not relevant so put them however you like. This is not an ideal solution. You may want things to be assignable based on only non-function properties, for example, implying state and not common functionality. You could try to extend the checking to account for that but there are pitfalls regardless.


I'm sorry to say I believe the only "true" answer is one that isn't viable yet. If you're working with classes you can use decorators to capture metadata about your application including type information that can be later used for reflection/inspection. You could create your own decorators to do this but you can also leverage typescript's own metadata generation for class properties. that you can turn on with a tsconfig flag.

This assumes the metadata reflection api is present. You'll need to use this to extract that information. You could then rewrite the isAssignableTo function from earlier to work off of the metadata. If I remember correctly, this still suffers from the issue that types are stored as strings and if you then have subtypes they won't match.


Anyway, I hope that helps. Sorry I couldn't give you a simple yes. Knowing my luck you were actually just looking for assignment ;).

Beret answered 17/5, 2016 at 6:45 Comment(1)
No, your interpretation was spot on. I did indeed want run-time assignment compatibility checking. The application is parsing a string from a database. It started out as a single primitive X. Then mutated into an array [X,Y,Z]. Then an object defining a comparator and an array of primitives { "Comparator": "any", "Values":[X,Y,Z] }. Depending on age a string could contain any of these things so I parse it as JSON and then work out what I got before normalising legacy encodings - a primitive or array of primitives provide the values and the comparator is "any".Spittoon
D
0

It is actually possible to implement a version of this, with classes at least:

export function isAssignableFrom(object:any, cls:any) {
    let proto = object.prototype
    while (proto != null) {
        if (proto instanceof cls) {
            return true
        }
        proto = proto.prototype
    }
    return false
}

with this method you can do things like:

class Foo {}
class Bar extends Foo {}

isAssignableFrom(Bar, Foo)   
// --> true

As discussed in the linked post, it's a bit niche, but we do use this in a couple of places in the JsPlumb codebase.

https://jsplumbtoolkit.com/talking-tech/2024/05/10/is-assignable-from

Disseise answered 10/5 at 7:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.