A
arktype•3mo ago
francis

Is there a way to perform schema parsing generically?

My reasoning is to integrate Arktype with https://effect.website/ This most simple example fails:
function genericDemo<T>(schema: Type<T>, u: unknown): T {
return schema.assert(u);
}
function genericDemo<T>(schema: Type<T>, u: unknown): T {
return schema.assert(u);
}
This fails as the return type of .assert is finalizeDistillation<T, _distill<T, { endpoint: "out"; }>> Is there a way to coerce this back to a T? Or is there some constraint I need to put on the T input to make this work?
Effect – The best way to build robust apps in TypeScript
Effect is a powerful TypeScript library designed to help developers easily create complex, synchronous, and asynchronous programs.
14 Replies
ssalbdivad
ssalbdivad•3mo ago
The parameter of a type can include morphs and constraints, so the inferred return type is correct there- you wouldn't want to return T
francis
francisOP•3mo ago
my reasoning is to create a function which can be used in an effect chain, i.e. something like this:
export const arktypeParseEffect =
<T>(schema: Type<T>) =>
(u: unknown): Effect.Effect<T, ArktypeParseError, never> => {
const res = schema(u);
if (res instanceof type.errors) {
return Effect.fail(new ArktypeParseError({ errors: res }));
}
return Effect.succeed(res);
};
export const arktypeParseEffect =
<T>(schema: Type<T>) =>
(u: unknown): Effect.Effect<T, ArktypeParseError, never> => {
const res = schema(u);
if (res instanceof type.errors) {
return Effect.fail(new ArktypeParseError({ errors: res }));
}
return Effect.succeed(res);
};
ah, hm so here's my problem: how do I get access to the finalizeDistillation type from the outside?
ssalbdivad
ssalbdivad•3mo ago
There are a few options. You could use type.infer.Out<T>
francis
francisOP•3mo ago
I could do this but it seems hacky:
export const arktypeParseEffect =
<T>(schema: Type<T>) =>
(u: unknown): Effect.Effect<ReturnType<typeof schema.assert>, ArktypeParseError, never> => {
const res = schema(u);
if (res instanceof type.errors) {
return Effect.fail(new ArktypeParseError({ errors: res }));
}
return Effect.succeed(res);
};
export const arktypeParseEffect =
<T>(schema: Type<T>) =>
(u: unknown): Effect.Effect<ReturnType<typeof schema.assert>, ArktypeParseError, never> => {
const res = schema(u);
if (res instanceof type.errors) {
return Effect.fail(new ArktypeParseError({ errors: res }));
}
return Effect.succeed(res);
};
aha, that's exactly what I need. thanks! ah hm, no, it's not
ssalbdivad
ssalbdivad•3mo ago
There are a lot of limitations in TS around inferring generic return types without casting even if the types are trivially identical
francis
francisOP•3mo ago
Type 'finalizeDistillation<T, _distill<T, { endpoint: "out"; }>>' is not assignable to type 'finalizeDistillation<inferDefinition<T, {}, {}>, _distill<inferDefinition<T, {}, {}>, { endpoint: "out"; }>>'.
Type 'finalizeDistillation<T, _distill<T, { endpoint: "out"; }>>' is not assignable to type 'finalizeDistillation<inferDefinition<T, {}, {}>, _distill<inferDefinition<T, {}, {}>, { endpoint: "out"; }>>'.
ssalbdivad
ssalbdivad•3mo ago
There may be a way we can get it to infer without casting by making the constraint a Type instead of the param though (if you cast there it would work because in reality those two types are equivalent) Hmm well I guess actually if it's not the type instance you're passing they may not be hold on
francis
francisOP•3mo ago
I'm sorry, I don't understand what you mean. Forget all the effect stuff - my very simple case is this:
function tryParseArktype<T>(schema: Type<T>, u: unknown): type.infer.Out<T> {
return schema.assert(u);
}
function tryParseArktype<T>(schema: Type<T>, u: unknown): type.infer.Out<T> {
return schema.assert(u);
}
if I can figure out what belongs as the return type of this function, that's the whole problem sorted
ssalbdivad
ssalbdivad•3mo ago
export const arktypeParseEffect = <t extends type.Any>(
schema: t,
data: unknown
): t["inferOut"] => {
return schema.assert(data)
}
export const arktypeParseEffect = <t extends type.Any>(
schema: t,
data: unknown
): t["inferOut"] => {
return schema.assert(data)
}
Would this work If you want to accept a type definition instead of an instantiated type in your API to remove a layer of indirection, there's also a way to do that
francis
francisOP•3mo ago
yes - that solves it, thank you! I am curious as to why Out<T> didn't do that - but no worries it does look like it should
ssalbdivad
ssalbdivad•3mo ago
Because it I made a mistake, it expects a definition or a type instance but we passed it the inner type param This would be another option:
export const arktypeParseEffect = <const def>(
def: type.validate<def>,
data: unknown
): type.infer.Out<def> => {
return type.raw(def).assert(data)
}
export const arktypeParseEffect = <const def>(
def: type.validate<def>,
data: unknown
): type.infer.Out<def> => {
return type.raw(def).assert(data)
}
Although best not to create a new type for each parse call so you'd want to curry or cache it That way you could pass {foo: "string"} or whatever directly instead of having to wrap everything in Type (although it also works if you wrap it in Type since types are valid definitions)
francis
francisOP•3mo ago
ah, well, I have types already I would reuse, so passing a Type in is the way to go 🙂
ssalbdivad
ssalbdivad•3mo ago
Sorry, just added .validate you'd need that to get autocomplete Cool, definitely hope this works out! I talk to Effect folks somewhat often so I'm sure they'd be interested haha
francis
francisOP•3mo ago
ha, it works! this is excellent
import type { HttpIncomingMessage } from "@effect/platform";
import { type, type ArkErrors } from "arktype";
import { Data, Effect } from "effect";

export class ArktypeParseError extends Data.TaggedError("@errors/ArktypeParseError")<{
errors: ArkErrors;
}> {}

export const arktypeParseEffect =
<T extends type.Any>(schema: T) =>
(u: unknown): Effect.Effect<T["inferOut"], ArktypeParseError, never> => {
const res = schema(u);
if (res instanceof type.errors) {
return Effect.fail(new ArktypeParseError({ errors: res }));
}
return Effect.succeed(res);
};

export const arktypeBodyJson = <T extends type.Any>(schema: T) => {
return <E>(
self: HttpIncomingMessage.HttpIncomingMessage<E>,
): Effect.Effect<T["inferOut"], E | ArktypeParseError, never> =>
Effect.flatMap(self.json, arktypeParseEffect(schema));
};
import type { HttpIncomingMessage } from "@effect/platform";
import { type, type ArkErrors } from "arktype";
import { Data, Effect } from "effect";

export class ArktypeParseError extends Data.TaggedError("@errors/ArktypeParseError")<{
errors: ArkErrors;
}> {}

export const arktypeParseEffect =
<T extends type.Any>(schema: T) =>
(u: unknown): Effect.Effect<T["inferOut"], ArktypeParseError, never> => {
const res = schema(u);
if (res instanceof type.errors) {
return Effect.fail(new ArktypeParseError({ errors: res }));
}
return Effect.succeed(res);
};

export const arktypeBodyJson = <T extends type.Any>(schema: T) => {
return <E>(
self: HttpIncomingMessage.HttpIncomingMessage<E>,
): Effect.Effect<T["inferOut"], E | ArktypeParseError, never> =>
Effect.flatMap(self.json, arktypeParseEffect(schema));
};
return httpClient.pipe(
Effect.andThen((client) => client.get(url)),
Effect.andThen(arktypeBodyJson(typeInstance)),
Effect.andThen((data) => {
// data is now strongly typed
}),
return httpClient.pipe(
Effect.andThen((client) => client.get(url)),
Effect.andThen(arktypeBodyJson(typeInstance)),
Effect.andThen((data) => {
// data is now strongly typed
}),
Want results from more Discord servers?
Add your server