1

Is it possible for runtime data to specify the type for a runtime type check? Hopefully using io-ts?

A switch statement creates more than one place to add new types. Looking up object properties like types[runtime.type] creates compile time type checking errors. Values could be undefined.

Runtime data:

[{label:"user", userid:1}, {label:"post", body:"lipsum"}]

Types:

type User = {
  label: 'user'
  userid: number
}

type Delete = {
  label: 'post'
  body: string
}

After checking the type, I want to also use the data in a generic implementation:

function save<A>(data:A) {
  mutate<A>(data)
  validate<A>(data)
  send<A>(data)
}
steve76
  • 302
  • 2
  • 9
  • 1
    Generally typescript is a compiler down to javascript, so nothing "typescript-y" is available at runtime. For how to differentiate types at runtime, see Type Guards (but it isn't going to work like you're suggesting): https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types – Rob Napier Aug 31 '20 at 20:39
  • 1
    I just remembered how fp-ts implements HKT with TypeScript module augmentation. If you scroll down or search, they implement a library wide dictionary and keep adding entries for each type, then just reference like a simple object property or dictionary. I'm going to try that and I'll let you know. https://gist.github.com/gcanti/2b455c5008c2e1674ab3e8d5790cdad5 – steve76 Aug 31 '20 at 21:12
  • I've created an experimental package called `ts-data-checker` which does type checking of runtime data (JSON strings or values) by running the typescript language service: https://www.npmjs.com/package/ts-data-checker – Markus Johnsson Jul 23 '21 at 11:48
  • I think it's best to just use switch. When I did this last year, I was going for extensible code. I've since moved onto the idea that if you want to program, program. Use version control and IDEs instead of creating an inner platform. – steve76 Jul 23 '21 at 16:48

2 Answers2

0

This is called correlated record types, and has quite a discussion in the TypeScript community. My approach is to move the correlated record check to runtime:

https://github.com/microsoft/TypeScript/issues/30581#issuecomment-686621101

https://repl.it/@urgent/runtime-fp-ot

Step 1

Create a general Prop interface holding all allowable properties.

interface Prop {
  label: "user" | "post",
  userid: string,
  body: string
}

This does prevent type specific properties, and you could have collisions. user.id wants to be a number but post.id wants to be a string. Not a big deal to me. Think of a different property name specific to your type, accept what's there, or if you are adventurous try adding a dimension to Props and indexing by type.

Step 2

Create a map of labels to runtime decoders. In typescript I use a class so I can extend it through different files:

class Decoder {
  user: userDecode,
  post: postDecode
}

Step 3

Create a function which takes props, looks up decoder from class prototype, and performs runtime decode

(props:Props) => Decoder.prototype[props.label].decode(props as any)

io-ts needs to widen here for any in strict mode. Also you need a check that props.label exists in Decoder

Step 4

Create similar maps of functions to run. If you call them after the runtime decode, you know runtime is passing valid values.

Cons

  1. Much more complex than writing out and managing switch statements. TypeScript type narrows for switch automatically.
  2. Property collisions. No specific properties to the type without added work.
  3. Need to manually check type exists. switch will ignore, and use a default case

Pros

  1. You can close the runtime processing for good. Isolated, no opening that code if you need to add support for different runtime types.
  2. When creating support for different runtime types, like page or file, something like babel file can import new files automatically.
  3. Good for version control and developer access permissions. New types can be opened up for public submission. Core processing can remain closed. So you have the start of a DSL.
steve76
  • 302
  • 2
  • 9
0

If your label is common among all of the types, you can handle this easily with type guards if you test for each case you want to handle.

For a function that is marked as returning x is y, if it returns true, then the compiler knows that in the true portion of an if, the variable is that type and in the else, not that type.

const data = [{label:"user", userid:1}, {label:"post", body:"lipsum"}]

type User = {
  label: 'user'
  userid: number
}

function isUser(u: { label: string }): u is User {
  return u.label === 'user';
}

type Delete = {
  label: 'post'
  body: string
}

function isDelete(d: { label: string }): d is Delete {
  return d.label === 'post';
}

for (const datum of data) {
  if (isUser(datum)) {
    console.log(`User with userid of ${datum.userid}`);
  } else if (isDelete(datum)) {
    console.log(`Delete with body of ${datum.body}`);
  }
}

TypeScript Playground
User Defined Type Guards

crashmstr
  • 28,043
  • 9
  • 61
  • 79
  • Thank you. Yes on type narrowing which works on control flow but not object indexing. Unfortunately I can't hardcode the values or have a big switch statement. I welcome feedback please on my approach below: – steve76 Sep 04 '20 at 19:14