Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Nit: they're not asking for runtime type safety; they're asking for the ability to reflect on types (at compile time, to generate values) so that they can use type information at runtime.

This helps ensure runtime type safety because (for example) it would be great to have a generic "validation" function that takes an arbitrary interface and an arbitrary object and validates that object. One way to implement this would be to use /compile time/ reflection to generate code (TS code hypothetical, because I write C++ nowadays):

  function validate<T>(obj: Any): T | null {
    switch constexpr (T) {
    case String:
      return typeof obj == "string" ? obj : null;
    case Array<U>
      if (!Array.isArray(obj)) {
        return null;
      }
      for (const u of obj) {
        if (validate<U>(u) == null) {
          return null;
        }
      }
      return obj;
    // ... more base cases
    }

    for (const prop: (keyof T) of Reflect<T>.Properties()) {
      if (validate<T[prop]>(obj[prop]) == null) {
        return null;
      }
    }

    return obj;
  }

  interface Date {
    year: String;
    month: String;
    day: String;
  }
It would be great if this could generate /JavaScript/ code:

  function validate__String__(obj) {
    return typeof obj == "string" ? obj : null;
  }
 
  function validate__Array$Date$__(obj) {
    if (!Array.isArray(obj)) {
      return null;
    }
    for (const u of obj) {
      if (validate__Date__(u) == null) {
        return null;
      }
    }
    return obj;
  }

  function validate__Date__(obj) {
    for (const prop of ["year", "month", "day"])) {
      if (validate__String__(obj[prop]) == null) {
        return null;
      }
    }
    return obj;
  }
Unfortunately this is not possible (AFAIK) in TypeScript currently, and will not be possible with TypeScript's current philosophy.

(The above example is a hypothetical TypeScript compiler that might support "templated" generic functions; with just RTTI TypeScript could accomplish the same thing in a non-templated function by passing in `T` as a function parameter at runtime and doing runtime comparisons on `T`.)



Not to be flip, but if it were really all this easy, we would have done it already.

There are dozens of questions you can throw at this code: What if the input's a union? What if it's a nested union -- how do you avoid combinatorial explosion? What if the input is a function -- how do you validate its parameter types using runtime information? What if the input is a conditional type? What if you're inside a generic function? The list is enormous and it quickly gets into "you've dug too deep and unleashed a Balrog" territory once you get beyond the primitives.


Why don't you just make the transformer API stable, public, and let the community do the hard part? There's plenty of us that have experimental transformers doing all sorts of fun things, these are problems that can be solved external to TS.

I've got a fully functional compile-time dependency injection container that I've been sitting on for literal years because the transformer API isn't public.


^^^ Please do this. I'm completely ok with "using transformers voids your nonexistent warranty" and the community can deal with transformer API churn. Exposing the API makes it easier to adopt those community solutions, versus me needing to explain to teammates why I switched all the `tsc` invocations to `ttsc` and promise it's not that sketchy.


> Not to be flip, but if it were really all this easy, we would have done it already.

Typescript is mature. There is no low hanging fruit to be develop.

I don't necessarily think it needs this but this is hardly the right grounds to dismiss it.


I do not think that they mean that all easy things have been done, rather that if this feature was easy then it would already exist


It is easy. It's just a terrible idea.

(So as it turns out having thought about it a bit, I am vehemently against this idea.)

Type reflection at runtime would require polluting the JS environment with a lot of cruft.

That might be a global object and lots of helper functions to query types. It might also be tagging objects and fields with additional properties that need to be treated as reserved.

There is certainly no way to do this that doesn't make assumptions about the runtime environment in a way that will cause a mountain of issues further down the line.

The other reason for my disdain is: the need to infer types at runtime is almost certainly indicative of an architectural issue with your code. If you aren't able to write viable code in a given context without requiring runtime type querying then you should step back from your intent and re-evaluate your code structure.


I mostly agree, having every single type checked all the time like other static languages is not appropriate and probably not even a good idea but on the other hand it is also kind of annoying to have your validation code be divorced from your types totally.

For example suppose I have a web request coming in and I have essentially just a string of input coming in on the body. I have an expectation of what it needs to be but there is a step where it is of type `unknown` and I need to convert it into `IMyHandlerInput`. Just casting it is obviously a bad idea and so now I need to essentially use some kind of library such as ajv to do json schema or jtd validation, _then_ I can cast it into my interface.

This is all fine but it definitely feels redundant to me. It would be a cool _ecmascript_ feature to essentially support, not runtime types per-se but syntactic _validation_ which can also be used by typescript to derive types automatically.

This is totally hypothetical but I'm imagining something like this for example:

```ts schema Example { @min 0 id: number

  @pattern /^\w+ \w+$/
  name: string

  @future
  expiresAt: Date
}

const data: Example = new Example(JSON.parse(body)) ```

Something that the JS runtime can use to allow developers to opt-in to extensible validation which can also be used by the typescript interpreter to essentially derive types.


Maybe I'm very mistaken, but to me it seems this code snippet is basically an alternative to writing a ton of "infer"-s and overloads, no? So the same pattern matching could be used in the "switch". Whatever the complier knows can be locally matched, and the combinations have to be already handled by the developer.


It's hard to come up with a compiler that produces a sound checker for arbitrarily complex union/intersection types. Perhaps there could be a restriction on reflection to "simple enough" types, but that's always going to be a weirdly moving target based on heuristics. There's already cases where Typescript tries to generate ~40MB+ .d.ts files which are just re-stating the types themselves. So it's easy to imagine a validator compiler emitting 100MB+ of code to check more wild and crazy types.


Size, computation and other budgets seem like useful knobs to expose to developers. And anything that's locally decidable (so can be run on multiple cores easily).


Given that several projects exist in python to do this… I guess if you know about types at runtime, it is possible to do it.

I write one of them (typedload).

If it's a union, it has a bunch of heuristics to guess right at the 1st try, but otherwise it will just try them all.


Can we just start with primitives and see what happens.


JavaScript already has this. It's called typeof.


Typescript purposefully influences this not at all.


And instanceof


Here's the thing though: if you wrote your TS properly, you don't need this and asking for it just highlights that you're not using TS the way it's meant to be used.

The only place you need runtime type enforcement (when you're writing your own code in TS) is for validating third party data at the point where you're ingesting it into your own code. Once it's in there, it is type safe if you used TS to compile your code to JS, because your function calls and data copies and everything else that moves data around was defined in terms of compatible shapes.

And we already have runtime validation libraries to cover that validation step. So many of them.


> And we already have runtime validation libraries to cover that validation step.

Right, but now my type information for inbound data must be in two places: the typescript type, and the validation schema. And heaven forbid I make a mistake and those two become out of sync.

Yes, I can use something like zod to define a schema then infer the type from it, but those inferred types are often … suboptimal to work with.


Zod is great, why do you think it is suboptimal ?


I love zod and use it daily.

It is also the most frequent thing where junior developers get stuck. TS tooling doesn't make it very easy to work with large extracted types though it is head and shoulders above other mainstream languages. The error messages often become incomprehensible walls of text once your types are Complex enough, and then tsserver will just truncate the type info in pop overs making them useless.

I am personally OK to live with all of the above. My single issue with zod is that it is not easy to use if you don't own your types. If your types are coming from a third party lib you don't have an easy path to derive runtypes from them. If the concept of runtypes was supported by the compiler itself, this could have been possible but as a userland library zod can't handle this easily.


Why would a ts-native offering produce better error messages when it comes to complex types? That is an issue with TS in general (or rather, with complex types in any typesafe language).


No.

Imagine a Zod schema like:

const UserLocation = z.object({ address: Address, coords: Coords })

where Address, Coords are other zod schemas.

Now if I infer a type like:

type IUserLocation = z.infer<typeof UserLocation>

Th inferred IUserLocation is something like:

type IUserLocation = { address: { city: string, country: string, ... }, coords: { lat: string, long: string } }

The extracted type does not refer to separate Address, Coords type, it is a single complex type that represents the complete nested structure.

So if I do something like:

const userLocation: IUserLocation = { address: { ... }, coords: { lat: 0, long: 0 }}

The error message is complex because it (effectively) says that coords.lat was expected to be a string from IUserLocation["coords"]["lat"] but it was number

However, if I define a ts interface directly:

interface IUserLocation { address: IAddress coords: ICoords }

now any errors reported against this interface will be more comprehensible because they will (effectively) say that coords I am providing does not comply with ICoords.

This is a contrived example, but this becomes more complex when you have deep compositions of zod types.

This issue of typescript not using well named intermediate types when types are "extracted" from schema definitions like zod is what makes the error messages complex.

This would not apply if typescript was to provide runtime type checking for interfaces defined in typescript (as opposed to them being inferred) because ts will have a way to "know" about these intermediate types. Then the type errors would not have to be always reported against the complex top level type.


That's a good point.

I just realized that I am working around this problem by explicitly setting a `z.Schema<...>` type: [link redacted]. The reason was to retain JSDoc comments on the types, but I guess this was another positive side effect.

In any case, I agree with you that there is room for improvement.


Ah ok. However, typescript is mostly smart enough to propagate comments [1] from zod schema properties to inferred types on its own.

Thanks for maintaining kanel btw. We used to use this in a previous role alongside knex. It was very useful.

[1] https://lorefnon.me/2022/06/25/generating-api-docs-for-zod-t...


Hah, I wasn't aware! I think that must have been fixed after I tried it the first time because I definitely concluded that it was necessary back then. And thank you for the kind words!


I found Zod a lot easier to use once we were given the "satisfies" keyword. I still basically have to write my schema twice (I don't mind this) but I can ensure the two are tightly coupled. A change to either one will show a compile error in the right place:

interface Person { firstName: string; lastName: string; }

const PersonSchema = z.object({ firstName: z.string(), lastName: z.string(), }) satisfies z.Schema<Person>;


Interesting! I'll probably start doing that. I hadn't connected the satisfies keyword to this problem, but it seems like a good solution.

My complaint about writing the schemas twice isn't the busywork (which is, like, annoying, but whatever I'll live). But the concern about small discrepancies between the two causing problems later.

I'll definitely use this going forward.


I like a lot of things about zod (it's what I use when I need to do this kind of validation), but when I was working with a mildly complicated schema the type that z.infer<Schema> produced wasn't great for me.

When I produce a type definition for a nested schema, I'll produce types for individual pieces.

So

type Schema = { foo: Foo; bar: Bar; baz: Baz[]; }

type Foo = { red: Color; white: Color; blue: Color; }

type Color = { r: number; g: number; b: number; }

type Bar = {...} type Baz = {...}

I couldn't find a clean way to decompose the type that z.infer<Schema> produced. It would give me a single type object that was deeply structured. For a pretty flat-simple small schema, z.infer<> was totally fine.

For the more complicated schema it wasn't _terrible_, I still used it. I made it work, but it definitely wasn't the dev experience I was hoping for.

I think something that went the other way would be much preferable for me. I'd rather define the typescript type, then have some validate<Schema>() function that was available to me.

Basically, I think it's _easier_ to get a generated validate() function to play nicely with the rest of my code, than it is to get the inferred type to play nicely with the rest of my code.


Use objectSchema.shape.xxx to access the nested schemas and infer on those.


Except for the fact that usually there is not just one single typescript agent working within itself, for which you don't need validation. There are many cases in which you need to verify some object, anything that does not come from your code i would argue is untrusted, I come across this almost daily, it would be absolutely fantastic to just have a way to check does this object conform to this type? Instead, i need to use some external dependency, effectively duplicate my type definitions and add yet another place to introduce bugs.


Just use a run-type transformer like typia, hooked right into typescript-compile. Get your runtime type validators generated from nothing but the typescript definitions.

Alternatively, use a runtime validator the provides good type inference out of the box so you're still only declaring your types once.


I would recommend `zod` as a good typescript schema runtime validation library as well. It does require strict type checking to be on however.


I use zod but it's a workaround. I use non standard syntax to define types.

If typescript emitted type information, we'd be able to automatically get runtime validation at the boundaries of our apps.


Alternatively, io-ts is more performant and has been around for a while longer. Although it does have a more functional interface.


I like typia for doing codegen basically in the way this request is asking: it hooks into the typescript API to create runtypes from plain old typescript


And presumably you need to also trust your third party libraries, unless you're also compiling them from TypeScript. Right?

And even if you are--that might also involve ensuring their tsconfig.json is compatibly similar to yours. Otherwise the compiler might allow them to return null instead of the object they say they return, among potentially many other "gotchas" that are bound to appear.

EDIT: Though I think I do agree with you, ultimately. Runtime type checking imposes non-negligible costs and complexity that still theoretically should be able to be guaranteed at compile time for cases where one isn't validating untrusted user input.


Yes, you need to trust your third party libraries, better check them. Otherwise they may steal your data, inject XSS, mine cryptos, have memory leaks or faulty logic.


> And we already have runtime validation libraries to cover that validation step. So many of them.

Did you read the article? I think that's the point. The premise is Typescript should be responsible for solution to runtime validation against Typescript not third party hacks.


It would be nice if the runtime validation was an ecmascript feature designed in such a way that typescript could infer the types for them automatically... Such as

    schema Example { ... }
    cosnt example: Example = new Example(unkonwnData)
Where a schema applied runtime validation based on declarative syntax and which typescript could use to derive a `type` from


That boundary validation is where that kind of reflection would be useful! And it's a problem that (nearly) every useful application has to face.

Well that, and getting rid of the silly "must be able to strip types without processing them" design ideology would also enable stuff like typeclasses.


> Once it's in there, it is type safe if you used TS to compile your code to JS […]

To the extend that TS's type-system is unsound, and everything may crash at runtime at random anytime.


In a language with a sound (or even sound modulo escape hatches) type system, I would agree. But Typescript can and will lie to you at compile time, and it's much harder to guard against this than it should be.


I still don't see the point. There is a long list of libraries that do exactly this, and do it well enough. The author has linked to them himself. The overall benefit of this would be maybe slightly better syntax for these libraries (even that is doubtful, because plenty of them already have a `reflect<T>()` interface), but still zero runtime benefit. And getting it to be accurate for 100% of cases would entail exactly what I mentioned – writing an entire runtime to do this inference.


So now you're stuck writing for an awkward inner platform language rather than using the TS syntax itself.


What's this "awkward inner platform language"? They are asking for a `typescript.generateRuntimeType<T>()` function to be native to the language. Well plenty of libraries provide exactly this syntax today. Here's tst-reflect: `const type = getType<T>()`. Notice any difference?


Wow, i didn't know that this is already possible with type script. seems like there are transformers you can add to typescript that can implement it.

So type script more or less supports the feature they asked for. Just not bundled with the main package, but they provide the interface to get it done with 3rd party transformers.


But tst-reflect seems to work by effectively building their own forked tsc compiler.


No, they have a plugin into the typescript compiler which uses their API. tsc already exposes all of the info they need.


Okay so it's not a forked compiler but it's adding a feature to the compiler.

At which point it makes a lot of sense to ask why that feature shouldn't be merged.


Because of feature creep. I don't need that feature. Don't merge things that can be kept separate. It is called modularity.


According to their (tst-reflect's) README, tsc doesn't support plugins. The plugin support comes from a fork called ttsc.


I just declare these structures with io-ts, and get validation, serialization, deserialization code and types for free.


That library is mentioned in the article


If that's so much needed I wonder why there isn't a thriving ecosystem of pluggable typescript preprocessors that add whatever values based on types direcly to typescript source before compilation.


There are a lot, and some are mentioned here in the comments.


It would be great if McDonalds would serve first class sushi, and pizza, and seafood, and pasta, and steak, and if they would deliver frozen meals, and iron your laundry, and ...

They just serve burgers and fries. And they are doing very well with that strategy. Same goes for TypeScript.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: