Devagamster
Devagamster

Reputation: 45

Typescript type guard through in operator

I was about to post an issue on github for this, but I figured I would ask here first.

I am working on a component entity system for a game I am writing using typescript. Component entity systems in javascript and (as far as I can tell) typescript tend to not be very type safe, and I have an idea to try to remedy that.

My current plan is to have each system which depends on a number of components expose an Entity type which will have a number of properties which entities that this system processes should have. I will then in a central place collect all of the various Entity types into one combined Entity union type.

export type Entity =
    CameraManagerEntity |
    CollisionManagerEntity |
    HoleManagerEntity |
    ParentManagerEntity | ...

Then in a system, I would like to be able to assert that some properties are on the entity and have the typescript infer that because the only type in the union which has that property on it is the type that my system exported, then it must be that exported type.

For example, say the CameraManager exported

interface CameraManagerEntity {
    foo: boolean,
    bar: number
}

And no other type in my Entity union has a foo parameter. My expectation is that if I have an instance of the Entity union, making an if statement which asserts that "foo" is in the instance should be enough to access bar on the instance without a type error.

function processEntity(entity: Entity) {
    if ("foo" in entity) {
        return entity.bar; // <-- Type error.
    }
}

Am I missing something? I think in an ideal world the compiler should have enough information to know that my entity object is a CameraManagerEntity from what it is given. It seems that what I am suggesting does not work as typescript exists today. Is there a better way to achieve what I am trying to do? Thanks in advance.

Edit: I am aware of user defined type guards, it would just be nice to not have to write the type guards out manually as I think the compiler should have all the information already.

Upvotes: 2

Views: 1134

Answers (1)

Robert Penner
Robert Penner

Reputation: 6398

What you want is almost like a Tagged Union Type turned inside-out. With a Tagged Union Type, all of the members share a common field, and TypeScript discriminates by checking the value of that field (usually with switch). But in your current design, the members of the union type are distinguished by the presence of unique fields.

I came up with a generic type guard that checks an entity against a type using the presence of a property:

if (hasKeyOf<CameraManagerEntity>(entity, 'camera')) {
  return entity.camera;
}

Here's the full example:

interface CameraManagerEntity {
  camera: string;
}

interface CollisionManagerEntity {
  collision: string;
}

type Entity = CameraManagerEntity | CollisionManagerEntity;

// Generic type guard 
function hasKeyOf<T>(entity: any, key: string): entity is T {
  return key in entity;
}

function processEntity(entity: Entity) {
  if (hasKeyOf<CameraManagerEntity>(entity, 'camera')) {
    return entity.camera;
  } else if (hasKeyOf<CollisionManagerEntity>(entity, 'collision')) {
    return entity.collision;
  } 
}

Try it in TypeScript Playground

But you might consider using Tagged Union Types for your entity system, which could be more ergonomic:

interface CameraManagerEntity {
  kind: 'CameraManager';
  camera: string;
}

interface CollisionManagerEntity {
  kind: 'CollisionManager';
  collision: string;
}

type Entity = CameraManagerEntity | CollisionManagerEntity;

function processEntity(entity: Entity) {
  switch (entity.kind) {
    case ('CameraManager'): return entity.camera;
    case ('CollisionManager'): return entity.collision;
  }
}

Try it in TypeScript Playground

Upvotes: 3

Related Questions