Hacker News new | ask | show | jobs
by eyelidlessness 1194 days ago
Types are absolutely available at runtime, you just have to start from the runtime. Type guards are the solution everyone wants +- a tiny bit of ceremony, and adding type system information to the runtime would just hurt performance for no good reason.

This:

  type Foo = {
    bar: string
    // …
  }
Can just as easily be addressed as:

  const parseFoo = parse.object({
    bar: parse.string,
    // …
  })

  type Foo = Foo<typeof parseFoo>
(Where parse methods here are type guards for their respective parsed return types, and can similarly be used to serialize runtime values as needed.)

For that little bit of extra ceremony, you get to isolate runtime stuff to the boundaries you need to care about, and let the type checker deal with everything else (typically most everything internal).

2 comments

Have you ever used a language where types can influence runtime behavior? It's an extremely powerful tool to do things like choosing a specialized sort algorithm based on the type of contents of array you are sorting. This has zero overhead iff types are known at compile time and can yet hit different code paths at runtime. To just dismiss this as not needed is extremely short sighted in my opinion.
Having types at runtime is a big deal and I lament that typescript can’t do it (for understandable reasons.)
I have. And I think the limitation is good for a language that doesn’t have that. I don’t want TypeScript to lie to me about its capabilities, I want it to help me make good decisions with the runtime code I actually have.
You can always use zod or io-TS.
> Types are absolutely available at runtime

TypeScript types are not available at runtime. As the name implies, runtime type information requires runtime support. Since TypeScript compiles to JavaScript, the only type information available is that provided by JavaScript.

Types are absolutely available at runtime. You just have to start with the runtime.

  const parseStr = (value: unknown): assert value is string => {
    if (typeof value === 'string') return value

    throw new Error(…)
  }

  const str = parseStr(anythingYouCanThrowAtIt)
I can 100% guarantee you str is going to have the same static and runtime type once you’ve checked it, or parsed it from whatever type you’d accept as a string. TypeScript won’t do the parsing for you, because JavaScript doesn’t have clear semantics for runtime casting that anyone wants or would accept. But type guards are exactly the solution to that and incredibly composable. Once you accept that reality and embrace it, getting the static type out of your runtime parser is a single added line of static type code. And all of this is almost exactly equivalent to what languages with runtime casts do, but you have complete visibility into it because you determine how casts behave. There are whole libraries which do this for you so don’t worry about rolling your own unless you have very particular needs. But TypeScript definitely has the facility to align static and runtime types however you see fit. You just need to tell the type system what types the runtime conveys, same as every other aspect of the type system.
You seem knowledgeable about TypeScript, so you certainly very well understand what people mean by "types are not available at runtime". Once your typescript is compiled to javascript, there's nothing left of your user-defined types and interfaces. Type guards are a security that is very much a workaround for the lack of types at runtime.
I’m encouraging thinking about the problem from a different angle. If you parse rather than validate[0][1], you have roughly all of the type information you could want in the runtime, and by using type guards the type system will reflect the runtime types.

It’s not a workaround. Every language with runtime types will have some logic devoted to this kind of casting/narrowing. TypeScript rightly doesn’t build it into the compiler because most of the time you don’t want types to have runtime behavior. For internal logic which can be validated statically, runtime types would be an unnecessary overhead. So it’s up to developers to determine where it should be applied. Type guards are explicitly an affordance for that, designed specifically to convey types with runtime casting/narrowing.

If you use good, composable primitives like zod or io-ts or any of several other implementations, your code will typically be almost identical to deriving runtime from types rather than the inverse.

0: https://itnext.io/parse-dont-validate-incoming-data-in-types...

1: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

You can use discriminated unions with TS (https://antman-does-software.com/typescripts-discriminated-u...), and you can even parse them from unknown data using tools like zod (https://zod.dev/?id=discriminated-unions), io-ts, or the likes.

I can't think of anything I can do in .NET (which has runtime types) that I can't do in TS with the help of a library or two.

> I can't do in TS with the help of a library or two.

Libraries like io-ts and zod embed the type information into JavaScript, so the information is available at runtime but only for wrapped types. If TypeScript provided runtime type information, such libraries wouldn't be necessary.