Closed stuft2 closed 1 year ago
@stuft2 Hi, this is an interesting one.
I think this could very well be a limitation in TypeScript where it can't quite resolve the static type through control flow analysis (where most of the type information in the example given is derived mostly through control flow), but it's an unusual one....
The issue appears to stem from the constrained generic argument T extends TSchema
not being observed as a TSchema
through control flow. My guess is that TypeScript makes the assessment that T
can be variant (and expects covariant types of TSchema), but when it tries to instantiate guard return types (i.e. TypeCheck<T extends TSchema>.Check(value: unknown): value is Static<T>
), it's can't make a call on the guards return signature given the potential for variance (and doesn't just default to TSchema
(which would infer as unknown
and would be expected (at least I think so))).
Because the issue appears to relate to the constrained generic type, control flow inference of that type, and inference issues instantiating multiple possible types of TSchema
in the guard (there's quite a bit going on here), you can side-step TypeScript's handling of TSchema
(as a generic) and just use it as a non-generic type. This will be enough to get the behavior you want at runtime, but you will need to be explicit about the return type.
return new Construct<Static<T>>(data.payload, data.description)
Quick example below of how you can approach this.
import {Type, Static, TSchema } from '@sinclair/typebox'
import { TypeCompiler } from '@sinclair/typebox/compiler'
class Construct<T = unknown> {
constructor (readonly payload: T, readonly description?: string) {}
}
// the schema is comprised of description and payload of non-generic TSchema
const createConstructSchema = (schema: TSchema) => Type.Object({
description: Type.String(),
payload: schema
})
// note: generic argument T is only used to infer the `Construct<Static<T>>` return type
function createConstructFactory<T extends TSchema> (constructDataSchema: T) {
const schema = createConstructSchema(constructDataSchema)
const check = TypeCompiler.Compile(schema)
return (data: unknown) => {
if (!check.Check(data)) {
throw 1
}
return new Construct<Static<T>>(data.payload, data.description)
}
}
const s = createConstructFactory(Type.Object({ x: Type.Number(), y: Type.Number() }))
// ^?
I might do a bit more digging here, but it's unlikely TypeBox can do much to help TS resolve things here (I've observed quite a few unusual rules with is
and assert
guards as they relate to control flow and generics). However when working with variant types of TSchema
, it can be helpful sometimes to operate in a "non-generic" way, but use TS to nudge the signatures you want from functions.
Hope this helps! S
@stuft2 Hi, might close this one off as there's likely not much TB can do to resolve this. The example provided should get around the issue however (explicitly treating the schema as TSchema
rather than relying on TS generics + control flow to derive the TSchema
). But it is a curious behavior (likely tied to TS covariance checks, as a guess), but as for resolving the issue through control flow (as per your implementation), I'm not sure this is possible (at least with the current TB setup)
Happy to discuss more on this thread (there's probably a pattern to document here around this particular limitation), so if you have follow on thoughts, let me know. But yeah, will close for now as there isn't much to action at this time.
All the best! S
I'm still experimenting with this kind of implementation. I'll try your suggestion and share anything I learn relevant to the conversation.
I'm trying to create a factory function which outputs various instantiations of a generic class. Here's a basic, contrived example:
I'm not sure why the error occurs exactly and I can't find any work arounds. Is this a bug or is it a feature that Typebox can't support yet do to Typescript limitations?