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

[docs] relation between JTDSchemaType and JTDDataType #1617

Closed
josh-hemphill opened this issue May 19, 2021 · 26 comments
Closed

[docs] relation between JTDSchemaType and JTDDataType #1617

josh-hemphill opened this issue May 19, 2021 · 26 comments

Comments

@josh-hemphill
Copy link

josh-hemphill commented May 19, 2021

What version of Ajv are you using? 8.4.0 Does the issue happen if you use the latest version? Yes

JTD Schema

export interface REQ {
	data: {
		text: string,
	}
};
export const Req: JTDSchemaType<REQ> = {
	properties: {
		data: {
			properties: {
				text: { type: 'string' },
			},
		},
	},
};

Your code

type ReqD = JTDDataType<typeof Req>
/* {
    data: {
        text: unknown;
    } & {};
} & {} */

type ReqD2 = JTDDataType<typeof Req.properties.data>
/*  {
    text: unknown;
} & {}; */

What results did you expect?

JTDDataType<typeof Req.properties.data>
// { text: string }

Are you going to resolve the issue?

If this is a bug, and I know where to start, sure.

@epoberezkin
Copy link
Member

I am not sure what is the intention, you might be using it incorrectly. It is not clear how the second sample relates to the first.

What is loginReq, for example?

Also, I don't think you can compare types with === - it is only for data comparison.

@josh-hemphill
Copy link
Author

josh-hemphill commented May 20, 2021

@epoberezkin Sorry, I was copying from my actual code and didn't finish renaming for the example.
I just used === to represent that that is what intellisense is showing those types to be.

@epoberezkin
Copy link
Member

I think I understand now... What's the version of typescript? It is only supported with >4.2.3

@josh-hemphill
Copy link
Author

josh-hemphill commented May 20, 2021

I don't have my project handy at the moment, but unless a new version of typescript has come out in the last few days, I'm using the latest stable version.
It's a fresh project.
And I have strict mode on in my tsconfig

@josh-hemphill
Copy link
Author

Yep, [email protected]

@josh-hemphill
Copy link
Author

Now I'm thinking it has to be something with my setup.
JTD discriminators don't seem to be working at all for me:

type RES = {
	ok: 1;
	err: string[];
	data: string;
} | {
	ok: 0;
	err: string[];
};
export const res: JTDSchemaType<RES> = {
	discriminator: 'ok', // Type 'string' is not assignable to type 'never'
	mapping: {
		'0': {
			properties: {
				err: {elements:{type: 'string'}},
			},
		},
		'1': {
			properties: {
				err: {elements:{type: 'string'}},
				data: {type: 'string'},
			},
		},
	},
};

Here's my tsconfig if it helps:

{
	"compilerOptions": {
		"outDir": "./build",
		"module": "es2020",
		"target": "es2020",
		"allowJs": true,
		"allowSyntheticDefaultImports": true,
		"strict": true,
		"checkJs": false,
		"moduleResolution": "node",
		"baseUrl": ".",
		"lib": [
			"ES2020"
		],
		"paths": {
			"#models/*": [
				"./models/*.js"
			],
			"#STypes/*": [
				"./types/*.js"
			],
			"##": [
				"./"
			]
		}
	},
	"exclude": [
		"build",
		"dist",
		"node_modules"
	]
}

@epoberezkin
Copy link
Member

It should be strictNullChecks: true, not strict

@epoberezkin
Copy link
Member

Although maybe strict does something else? But you definitely need strictNullChecks for it to work.

@josh-hemphill
Copy link
Author

Having strict enabled should enable strictNullChecks

"strict": true,
/* Enable all strict type checking options.
See more: https://www.typescriptlang.org/tsconfig#strict */

@josh-hemphill
Copy link
Author

Just tried explicitly enabling, still having the same issues.

@epoberezkin
Copy link
Member

ah - I think for JTDDataType to work schema must be defined "as const", otherwise types are too generic for it to construct data type.

@epoberezkin
Copy link
Member

@epoberezkin
Copy link
Member

but it does not seem to help...

@josh-hemphill
Copy link
Author

josh-hemphill commented May 20, 2021

Yep that was partly it. I was trying to use JTDDataType to test the returned type of JTDSchemaType.
After just inspecting the type returned it is showing correct there after adding as const.
I guess there's just an issue using JTDDataType on something that's already wrapped with JTDSchemaType, then the data type shows undefined for all nested types.

@epoberezkin
Copy link
Member

Correct - just tested it - they are not "reversible".

If you just pass the object "as const" you can derive JTDDataType from its type.

But not from the type constructed by JTDSchemaType - it's a different more complex type that what typeof ({...} as const) creates

@epoberezkin
Copy link
Member

You could test some extends relationships between these types - it probably can be formalised a bit what extends what.

cc @erikbrinkman

@epoberezkin epoberezkin changed the title Nested JTD types not being detected [docs] relation between JTDSchemaType and JTDDataType May 20, 2021
@josh-hemphill
Copy link
Author

I think most of this came from messing with it trying to find a way to have typed wrappers for my schemas.
Is there a way to have something like SomeJTDSchemaType on the JTD schema object to validate it when writing it but then still be able to derive JTDDataType? That would seem to be the sweet spot.

@erikbrinkman
Copy link
Collaborator

Thanks for working through this mostly without bugging me, but feel free to tag me earlier @epoberezkin. There's a few things to address.

First, for discriminated unions, JTD spec only allows strings. If you change your "ok" numbers to strings, the schema checks out. This isn't normal javascript nonsense where types get coerced all the time.

Second, there is a SomeJTDSchemaType to check the general schema objects:

export type SomeJTDSchemaType = (
| // ref
{ref: string}
// primitives
| {type: NumberType | StringType | "boolean"}
// enum
| {enum: string[]}
// elements
| {elements: SomeJTDSchemaType}
// values
| {values: SomeJTDSchemaType}
// properties
| {
properties: Record<string, SomeJTDSchemaType>
optionalProperties?: Record<string, SomeJTDSchemaType>
additionalProperties?: boolean
}
| {
properties?: Record<string, SomeJTDSchemaType>
optionalProperties: Record<string, SomeJTDSchemaType>
additionalProperties?: boolean
}
// discriminator
| {discriminator: string; mapping: Record<string, SomeJTDSchemaType>}
// empty
// NOTE see the end of
// https://github.com/typescript-eslint/typescript-eslint/issues/2063#issuecomment-675156492
// eslint-disable-next-line @typescript-eslint/ban-types
| {}
) & {
nullable?: boolean
metadata?: Record<string, unknown>
definitions?: Record<string, SomeJTDSchemaType>
}

Finally, and unfortunately, there can't be a nice correspondence between JTDSchemaType and JTDDataType and the answer has to do with unions. JTDSchemaType<string> isn't { type: "string" } (even ignoring metadata), it's { type: "string" | "timestamp" } due to the way that schemas are handled. From the outset JTDDataType was not designed to handle unions, because it's not clear how they should be handled. For the same reason that JTDDataType<{ type: "timestamp" }> isn't Date but Date | string. These ambiguities prevent a bijection between JTD schema- and data-space, and therefore prevent the kind of checking you desire.

@josh-hemphill
Copy link
Author

Do you know if there's a way to use a conditional type to only apply the SomeJTDSchemaType if it's invalid?
I've been trying to create a wrapping function that applies the constraint of SomeJTDSchemaType only if it's invalid, so that I get TS errors in VS Code when writing the schema, but as long as it's valid then it just passes through the object so I can use it with JTDDataType.
Something like:

export const getREQ_WRAPPER = <T>(
	v: SomeJTDSchemaType extends T ? T : SomeJTDSchemaType,
) => ({
	properties:{
		data: v,
	},
} as const);

But after trying every combination I can't seem to get that to work.

@erikbrinkman
Copy link
Collaborator

I'm not sure exactly what you're asking for, can you break it down a little bit to say exactly what you're writing, and where you want it to error? Unfortunately typescript doesn't have a way to throw an error with a conditional type (yet). Your two real options are to assign it to a string literal, or assign it to never, which will only throw once you try to set some value to it.

If there problem is with the output of JTDDataType, then maybe you want to assign datatypes with unknown to never instead? You should be able to do that with something like JTDSchemaType, but preserve the type or assign to never. Hoewever, I'm not sure that's what you're asking.

@josh-hemphill
Copy link
Author

As a side effect of wrapping my schemas I was hoping I could also use an enforced argument constraint to only apply SomeJTDSchemaType when v is invalid compared to it.
Essentially:
If v is SomeJTDSchemaType, then v must be v (it's just itself, and the const gets passed through, it doesn't get cast to SomeJTDSchemaType, and JTDDataType should work just fine with the function's returned type)
But if v is not SomeJTDSchemaType, then v must be (is constrained to be) SomeJTDSchemaType, so if I'm passing an object literal to the function, it gives me syntax errors where my argument does not match SomeJTDSchemaType.

const x = getREQ_WRAPPER({
	text:{type:'string'}
} as const)
/*
typeof x is
{
    readonly properties: {
        readonly data: {
            readonly text: {
                readonly type: "string";
            };
        };
    };
}
*/
const y = getREQ_WRAPPER({
	invalid: 'hi',
	/* Argument of type '{ readonly invalid: "hi"; readonly text: { readonly type: "string"; }; }' is not assignable to parameter of type 'SomeJTDSchemaType'.
  Object literal may only specify known properties, and 'invalid' does not exist in type 'SomeJTDSchemaType'. */
	text:{type:'string'}
} as const)
/*
typeof y is
{
    readonly properties: {
        readonly data: SomeJTDSchemaType;
    };
}
*/

@erikbrinkman
Copy link
Collaborator

There are a few solutions that I think achieve what you're trying to do.

The most trypscript way would be to just have compile / validate only take valid schemas. Unfortunately ajv is pretty permissive from a typescript font making this hard to work out. However you could define your own wrapper like:

function compileWrapper<T extends SomeJTDSchemaType>(schema: T): (obj: unknown) => obj is JTDDataType<T> {
    return ajv.compile(schema);
}

You could actually return the validator interface, but that's the gist. Note, all you're doing is restricting the actual function overloads:

ajv/lib/core.ts

Lines 359 to 362 in be07d3d

compile<N extends never, T extends SomeJTDSchemaType>(
schema: T,
_meta?: boolean
): ValidateFunction<JTDDataType<T>>

alternatively, you could just create a dummy checker variable:

const invalid = {
    properties: {
        invalid: 'hi',
        text: { type: 'string' }
    }
} as const;
const _: SomeJTDSchemaType = invalid;

will error when trying to assign to _, this is the close to a type assertion in typescript as it exists right now.

Finally, if you still want your getREQ_WRAPPER function I think you can just do:

const getREQ_WRAPPER= <T extends SomeJTDSchemaType>(x: T): T => x;

and that will preserve the type while also verifying that it's SomeJTDSchemaType but it's not very Typescripty, so I'd generally advise against it unless you think it's necessary.

@erikbrinkman
Copy link
Collaborator

@epoberezkin you updated this to a docs issue. Which aspect of this do you think needs to be documented? I'm happy to submit the PR just not sure what you think should be included.

@epoberezkin
Copy link
Member

@erikbrinkman I don't think it needs to be explicitly mentioned in the docs tbh - up to you.

I usually mark "docs" the issues that are themselves offer some useful information - whether it should be in the docs is the question of how many people need this info...

@erikbrinkman
Copy link
Collaborator

@josh-hemphill did one of those solutions work? Is this resolved?

@erikbrinkman
Copy link
Collaborator

closing due to inactivity, but feel free to reopen if this isn't resolved @josh-hemphill

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

No branches or pull requests

3 participants