Simple question re a function that returns a type:

Trying to create simple "factory" functions that make a type based on some other type. This works fine:
const makeBar = <T1, T2>(inputs: Type<T1>, outputs: Type<T2>) => type({
weird: "boolean",
in: inputs,
out: outputs
})

const Custom1 = type({
bar: "number"
})

const Custom2 = type({
foo: "string.integer"
})

const Other = makeBar(Custom1, Custom2)
const makeBar = <T1, T2>(inputs: Type<T1>, outputs: Type<T2>) => type({
weird: "boolean",
in: inputs,
out: outputs
})

const Custom1 = type({
bar: "number"
})

const Custom2 = type({
foo: "string.integer"
})

const Other = makeBar(Custom1, Custom2)
But if it's "string.integer.parse" it fails to compile. Any hint as to why? Do I have a problem with my makeBar() prototype?
26 Replies
TizzySaurus
TizzySaurus2mo ago
Wdym by "fails to compile"? Do you get a runtime error? If so, what's the error?
Mike Radford
Mike RadfordOP2mo ago
Compilation error:
error TS2345: Argument of type 'Type<{ foo: (In: string) => To<number>; }, {}>' is not assignable to parameter of type 'Type<{ foo: string; }, {}>'.
The types of 'readonly().map' are incompatible between these types.
Type '<transformed extends listable<MappedTypeProp>, r = Type<{ [k in keyof intersectUnion<{ [k in keyof ({ [prop in (transformed extends array ? transformed : [transformed])[number] as Extract<applyHomomorphicOptionality<{ readonly foo: (In: string) => To<...>; }, prop>, { ...; }>["key"]]: prop["value"][" arkInferred"]; ...' is not assignable to type '<transformed extends listable<MappedTypeProp>, r = Type<{ [k in keyof intersectUnion<{ [k in keyof ({ [prop in (transformed extends array ? transformed : [transformed])[number] as Extract<applyHomomorphicOptionality<{ readonly foo: string; }, prop>, { ...; }>["key"]]: prop["value"][" arkInferred"]; } & { [prop in (t...'.
Types of parameters 'flatMapEntry' and 'flatMapEntry' are incompatible.
Types of parameters 'entry' and 'entry' are incompatible.
Type 'BaseTypeProp<"required", "foo", (In: string) => To<number>, {}>' is not assignable to type 'BaseTypeProp<"required", "foo", string, {}>'.
Type '(In: string) => To<number>' is not assignable to type 'string'.
error TS2345: Argument of type 'Type<{ foo: (In: string) => To<number>; }, {}>' is not assignable to parameter of type 'Type<{ foo: string; }, {}>'.
The types of 'readonly().map' are incompatible between these types.
Type '<transformed extends listable<MappedTypeProp>, r = Type<{ [k in keyof intersectUnion<{ [k in keyof ({ [prop in (transformed extends array ? transformed : [transformed])[number] as Extract<applyHomomorphicOptionality<{ readonly foo: (In: string) => To<...>; }, prop>, { ...; }>["key"]]: prop["value"][" arkInferred"]; ...' is not assignable to type '<transformed extends listable<MappedTypeProp>, r = Type<{ [k in keyof intersectUnion<{ [k in keyof ({ [prop in (transformed extends array ? transformed : [transformed])[number] as Extract<applyHomomorphicOptionality<{ readonly foo: string; }, prop>, { ...; }>["key"]]: prop["value"][" arkInferred"]; } & { [prop in (t...'.
Types of parameters 'flatMapEntry' and 'flatMapEntry' are incompatible.
Types of parameters 'entry' and 'entry' are incompatible.
Type 'BaseTypeProp<"required", "foo", (In: string) => To<number>, {}>' is not assignable to type 'BaseTypeProp<"required", "foo", string, {}>'.
Type '(In: string) => To<number>' is not assignable to type 'string'.
TizzySaurus
TizzySaurus2mo ago
Does
<T1 extends type.Any, T2 extends type.Any>(inputs: T1, outputs: T2) => ...
<T1 extends type.Any, T2 extends type.Any>(inputs: T1, outputs: T2) => ...
work?
Mike Radford
Mike RadfordOP2mo ago
that doesn't even compile without the .parse :
src/test.ts:14:3 - error TS2322: Type 'T1' is not assignable to type 'validateProperty<T1, "required", {}, bindThis<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }>>'.
Type 'Any<any, any>' is not assignable to type 'validateProperty<T1, "required", {}, bindThis<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }>>'.

14 in: inputs,
~~

src/test.ts:14:3
14 in: inputs,
~~~~~~~~~~
The expected type comes from property 'in' which is declared here on type 'validateObjectLiteral<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }, {}, bindThis<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }>>'

src/test.ts:15:3 - error TS2322: Type 'T2' is not assignable to type 'validateProperty<T2, "required", {}, bindThis<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }>>'.
Type 'Any<any, any>' is not assignable to type 'validateProperty<T2, "required", {}, bindThis<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }>>'.

15 out: outputs
~~~

src/test.ts:15:3
15 out: outputs
~~~~~~~~~~~~
The expected type comes from property 'out' which is declared here on type 'validateObjectLiteral<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }, {}, bindThis<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }>>'
src/test.ts:14:3 - error TS2322: Type 'T1' is not assignable to type 'validateProperty<T1, "required", {}, bindThis<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }>>'.
Type 'Any<any, any>' is not assignable to type 'validateProperty<T1, "required", {}, bindThis<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }>>'.

14 in: inputs,
~~

src/test.ts:14:3
14 in: inputs,
~~~~~~~~~~
The expected type comes from property 'in' which is declared here on type 'validateObjectLiteral<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }, {}, bindThis<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }>>'

src/test.ts:15:3 - error TS2322: Type 'T2' is not assignable to type 'validateProperty<T2, "required", {}, bindThis<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }>>'.
Type 'Any<any, any>' is not assignable to type 'validateProperty<T2, "required", {}, bindThis<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }>>'.

15 out: outputs
~~~

src/test.ts:15:3
15 out: outputs
~~~~~~~~~~~~
The expected type comes from property 'out' which is declared here on type 'validateObjectLiteral<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }, {}, bindThis<{ readonly weird: "boolean"; readonly in: T1; readonly out: T2; }>>'
Is this a common pattern to have a function that takes a type and returns a new type?
TizzySaurus
TizzySaurus2mo ago
I'd say so, yeah. There's even an example of it on the docs: https://arktype.io/docs/generics#external
ArkType
ArkType Docs
TypeScript's 1:1 validator, optimized from editor to runtime
Mike Radford
Mike RadfordOP2mo ago
interesting ok not sure how I missed it.
TizzySaurus
TizzySaurus2mo ago
Does this work if you explicitly type the return? I.e. Type<{weird: boolean, in: T1, out: T2}>
Mike Radford
Mike RadfordOP2mo ago
worth noting that just going one additional level also break compilation:
const makeFoo = <T1, T2>(inputs: Type<T1>, outputs: Type<T2>) => type({
in: inputs,
out: outputs
})

const makeBar = <T1, T2>(inputs: Type<T1>, outputs: Type<T2>) => type({
weird: "boolean",
in: inputs,
out: outputs
foo: makeFoo(inputs, outputs) // <-- this line gets an error
})
const makeFoo = <T1, T2>(inputs: Type<T1>, outputs: Type<T2>) => type({
in: inputs,
out: outputs
})

const makeBar = <T1, T2>(inputs: Type<T1>, outputs: Type<T2>) => type({
weird: "boolean",
in: inputs,
out: outputs
foo: makeFoo(inputs, outputs) // <-- this line gets an error
})
Let me try I don't really know how to get that type
TizzySaurus
TizzySaurus2mo ago
As in
const makeFoo = <...>(inputs: T1, outputs: T2): Type<{weird: boolean, in: T1, out: T2}> => ...
const makeFoo = <...>(inputs: T1, outputs: T2): Type<{weird: boolean, in: T1, out: T2}> => ...
Just curious if that makes it work
Mike Radford
Mike RadfordOP2mo ago
oh I see it works in the string.integer setup, but adding .parse still breaks compilation
TizzySaurus
TizzySaurus2mo ago
Yeah, not sure then I'm afraid. Maybe @ssalbdivad can give some insight
ssalbdivad
ssalbdivad2mo ago
Hmm I'm really not sure why this comparison is even happening- seems like a possible TS bug? Maybe @Andarist would have some insight from that perspective. A workaround is to accept type.cast instead, which is basically "anything arktype recognizes as inferred like X" without checking all the type methods etc. which simplifies the inference and seems to work here:
import { type } from "arktype"

const makeBar = <T1, T2>(inputs: type.cast<T1>, outputs: type.cast<T2>) =>
type({
weird: "boolean",
in: inputs,
out: outputs
})

const Custom1 = type({
bar: "number"
})

const Custom2 = type({
foo: "string.integer.parse"
})

const Other = makeBar(Custom1, Custom2)
import { type } from "arktype"

const makeBar = <T1, T2>(inputs: type.cast<T1>, outputs: type.cast<T2>) =>
type({
weird: "boolean",
in: inputs,
out: outputs
})

const Custom1 = type({
bar: "number"
})

const Custom2 = type({
foo: "string.integer.parse"
})

const Other = makeBar(Custom1, Custom2)
Andarist
Andarist2mo ago
looks buggy to me but who knows
Andarist
Andarist2mo ago
usually i'd say the return type inference is at fault here but explicit type arguments turn off inference altogether so 🤷‍♂️ as always - I could investigate a small repro case of this 😛 fwiw, it's ur weird : r extends infer _ ? _ : never return type that breaks this and ofc a regular NoInfer fixes this issue:
diff --git a/ark/type/type.ts b/ark/type/type.ts
index c2cb322ff..149845cc2 100644
--- a/ark/type/type.ts
+++ b/ark/type/type.ts
@@ -63,7 +63,7 @@ export interface TypeParser<$ = {}> extends Ark.boundTypeAttachments<$> {
// Parse and check the definition, returning either the original input for a
// valid definition or a string representing an error message.
def: type.validate<def, $>
- ): r extends infer _ ? _ : never
+ ): NoInfer<r>

/**
* Create a {@link Generic} from a parameter string and body definition.
diff --git a/ark/type/type.ts b/ark/type/type.ts
index c2cb322ff..149845cc2 100644
--- a/ark/type/type.ts
+++ b/ark/type/type.ts
@@ -63,7 +63,7 @@ export interface TypeParser<$ = {}> extends Ark.boundTypeAttachments<$> {
// Parse and check the definition, returning either the original input for a
// valid definition or a string representing an error message.
def: type.validate<def, $>
- ): r extends infer _ ? _ : never
+ ): NoInfer<r>

/**
* Create a {@link Generic} from a parameter string and body definition.
Mike Radford
Mike RadfordOP2mo ago
nice! does this address the original issue? I just updated the playground link above to include the other line that fails to compile.
ssalbdivad
ssalbdivad2mo ago
@Andarist GODDAMN IT. Doesn't NoInfer end up displaying in the final type definition if I use it there though?
Andarist
Andarist2mo ago
U gotta decide what’s better I’d take NoInfer displayed over that issue And then maybe open an issue about NoInfer being displayed or smth - if u dont like it
ssalbdivad
ssalbdivad2mo ago
Is there a more minimal repro for it than this? I just wonder how much this comes up vs. polluting the hover of every type created from a chained method Yeah I was thinking about doing that. I still find all this return type inference stuff so unintuitive IIRC there was no external way to extract the NoInfer once it was there
Andarist
Andarist2mo ago
More minimal than what?
ssalbdivad
ssalbdivad2mo ago
Than a function that accepts two types like this? As someone who doesn't know what is actually going on in the compiler here it feels very broken
Andarist
Andarist2mo ago
No idea, i aint extracting a minimal repro out of this - if u do it i could investigate it
ssalbdivad
ssalbdivad2mo ago
Yeah fair I just didn't know if you had already stumbled on one
Andarist
Andarist2mo ago
But i cant commit to reducing ur complex types
ssalbdivad
ssalbdivad2mo ago
@Mike Radford did you try the version I mentioned? that seems to work fine for now

Did you find this page helpful?