Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Covariance issue with TypeCheck<TSchema> and TypeCheck<TObject<{}>> #925

Closed
patsissons opened this issue Jul 4, 2024 · 1 comment
Closed

Comments

@patsissons
Copy link

I am running into a covariance issue and I have reduced the problem down to this code:

import { type TypeCheck, TypeCompiler } from "@sinclair/typebox/compiler";
import { type TSchema, Type } from "@sinclair/typebox";

function validate(validator: TypeCheck<TSchema>, value: unknown) {
  return validator.Check(value);
}

// error
validate(TypeCompiler.Compile(Type.Object({})), {});
// no error
validate(TypeCompiler.Compile<TSchema>(Type.Object({})), {});

we can see that if we forcefully reduce the TypeCheck down with an explicit generic cast we resolve the error, but TObject<T extends TProperties = TProperties> extends TSchema and class TypeCheck<T extends TSchema> so this shouldn't be an issue. I don't have a good solution for this yet, but this is the most basic type normalizer i can build

function normalizeValidatorType<Validator>(validator: Validator) {
  return validator as TypeCheck<TSchema>;
}

validate(normalizeValidatorType(TypeCompiler.Compile(Type.Object({}))), {});

interestingly, if we change the normalizer generic constraint to Validator extends TypeCheck<TObject> then we can no longer cast back to TypeCheck<TSchema> because of the covariance issue.

@sinclairzx81
Copy link
Owner

@patsissons Hi, Apologies for the extended delay in reply (have had a exceptionally busy past few weeks)

interestingly, if we change the normalizer generic constraint to Validator extends TypeCheck then we can no longer cast back to TypeCheck because of the covariance issue.

Unfortunately, I don't have a good solution to covariance issues as TypeBox was originally built inline with TypeScript's structural view of the world, but where schematics may not naturally structurally extend other schematics in a way TypeScript can reason about a schematic being covariant with another (even if the schematic is representative of a type that would be considered covariant to the type system). This is a ongoing area of research.

This said, you can make things work if you just make the type argument for TypeCheck generic, as follows.

import { TypeCompiler, TypeCheck } from '@sinclair/typebox/compiler'
import { Type, TSchema } from '@sinclair/typebox'

function validate<T extends TSchema>(validator: TypeCheck<T>, value: unknown) {
  return validator.Check(value)
}

// no error
validate(TypeCompiler.Compile(Type.Object({})), {})
// no error
validate(TypeCompiler.Compile<TSchema>(Type.Object({})), {})

This is probably the best TypeBox can do at this point in time (and is a very common pattern when using the library). It should satisfy the majority of cases (but open to reviewing cases where this pattern doesn't meet general requirements).

Will close up this issue for now as the above is the general recommendation, but happy to discuss covariance / contravariance design aspects on this thread or via GH discussion threads.

All the best!
S

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants