A
arktypeā€¢7mo ago
JameEnder

Mapping types

Hello, is it possible do to code like this?
const x = type({
a: 'string',
b: 'string',
c: 'string'
})

const xType = typeof x;

const y = x.map((original) => ({
...original,
c: 'number'
}))

const yType = typeof y;
const x = type({
a: 'string',
b: 'string',
c: 'string'
})

const xType = typeof x;

const y = x.map((original) => ({
...original,
c: 'number'
}))

const yType = typeof y;
And if so, is it possible to also inherit those types while mapping in logic, like so?
const y = x.logicMap((original) => ({
...original,
c: Number(original.c)
}))

const yType = typeof y;
const y = x.logicMap((original) => ({
...original,
c: Number(original.c)
}))

const yType = typeof y;
89 Replies
ssalbdivad
ssalbdivadā€¢7mo ago
You can merge two types like this:
const user = type({ isAdmin: "false", name: "string" })
const admin = type({ "...": user, isAdmin: "true" })
const user = type({ isAdmin: "false", name: "string" })
const admin = type({ "...": user, isAdmin: "true" })
It works just like spread in JS You'll have to explain more about what you mean for c: Number(original.c)
JameEnder
JameEnderOPā€¢7mo ago
Merge could be used for the first example Well, lets say that I have a transformer like this
(original) => ({...original, c: Number(original.c)})
(original) => ({...original, c: Number(original.c)})
This takes in X, and returns Y, where the only difference between X and Y is that c is of type number, not string I wonder if A. Arktype has these transformers available somehow B. If they represent the output types correctly, according to the returns of the transformer
ssalbdivad
ssalbdivadā€¢7mo ago
Are you wanting to transform only between two types, or do you mean that you want a type that takes in a string and converts it to a number?
JameEnder
JameEnderOPā€¢7mo ago
Between two types, X and Y, and it figuring out automatically that Y is supposed to have c of number, not string But still keeping all the X fields that arent c
ssalbdivad
ssalbdivadā€¢7mo ago
Isn't that what the first example would do as well?
JameEnder
JameEnderOPā€¢7mo ago
Well not really, because I'm telling that first example the exact type But the second one is deriving it from the functionality and the original type
ssalbdivad
ssalbdivadā€¢7mo ago
Maybe if you can give an example where something other than Number is occurring because I don't really see how anything is being derived
JameEnder
JameEnderOPā€¢7mo ago
While Number(original.c) is a very simple and straight forward example, there may be much more complex transformations that I can't derive in my head Sure
ssalbdivad
ssalbdivadā€¢7mo ago
Ohh I think I see what you mean you want to be able to access the prop of an existing type The best way to do that for now is to define that prop as a standalone type and reference it in both places, then you can transform it one of them which is a good practice in general as if you're wanting to reuse it, it's generally best it's defined with a clear name in a reusable way There will be index access types soon but not yet. Also native map syntax that matches TS is planned
JameEnder
JameEnderOPā€¢7mo ago
const user = type({
name: string,
age: number,
})

const userWithAutoEmail = user.map((original) => ({
...original,
email: `${original.name}${original.age}@lol.com`
}))

typeof userWithAutoEmail.infer == {
name: string
age: number
email: string
}
const user = type({
name: string,
age: number,
})

const userWithAutoEmail = user.map((original) => ({
...original,
email: `${original.name}${original.age}@lol.com`
}))

typeof userWithAutoEmail.infer == {
name: string
age: number
email: string
}
And if userWithAutoEmail could be also called as a function that actually maps it, that would be perfect
ssalbdivad
ssalbdivadā€¢7mo ago
If you want to add a comment to https://github.com/arktypeio/arktype/issues/584 with an example/suggested API that will be helpful for when it's implemented
GitHub
Mapped types Ā· Issue #584 Ā· arktypeio/arktype
I created the following props rule type to support intersections/unions and eventually checking types whose values are dependent on associated keys. Some parts of this may end up being out of date,...
ssalbdivad
ssalbdivadā€¢7mo ago
For now would recommend splitting up anything you want to reuse though!
JameEnder
JameEnderOPā€¢7mo ago
I'm mostly asking this because of this TS issue
JameEnder
JameEnderOPā€¢7mo ago
No description
JameEnder
JameEnderOPā€¢7mo ago
Which I thought could be fixed by ArkType actually being a little smarter than plain TS
ssalbdivad
ssalbdivadā€¢7mo ago
Maybe in some cases. It's good with cyclic types but obviously some aspects of TS we have to work within. Will be interested to see!
JameEnder
JameEnderOPā€¢7mo ago
Okay update, I have been pointed to using Zod or Arktype for my issue on the Typescript Discord, exactly as I wanted :D
No description
ssalbdivad
ssalbdivadā€¢7mo ago
Haha, I don't think he likes ArkType very much IIRC šŸ˜…
JameEnder
JameEnderOPā€¢7mo ago
Do you have any ETA on when that "mapping" syntax is going to be available in ArkType?
ssalbdivad
ssalbdivadā€¢7mo ago
Hopefully within a few months. The first priority is to get all the 2.0 features documented and to finish generics + pattern matching But once generics are done it should be a relatively clean abstraction on top of that Even though they're less fun I also have to prioritize stuff like generating JSON schema/ open API integration etc. because that is what everyone asks for LIke I said though add a comment to the issue the more feedback I get on stuff the more I will prioritize it
JameEnder
JameEnderOPā€¢7mo ago
I'm not sure if I used to word correctly, as the issue you sent me is talking about "mapped" types, where I'm more interested in "transforming" and getting its output types at the same type So both runtime and typescript level
ssalbdivad
ssalbdivadā€¢7mo ago
Well that is what mapped types do right? Everything in arktype is both at runtime and TS level
JameEnder
JameEnderOPā€¢7mo ago
I'm not sure I'll have to think about it some more Only if Typescript could include such an obvious feature as recursive datatype inheritance... well
ssalbdivad
ssalbdivadā€¢7mo ago
There is a very general transform API that gives you total control over mapping all the types nodes in @arktype/schema but that is a more advanced feature and it wouldn't be possible to infer from that API But I use that internally to e.g. prune checks that have already been discriminated from types, so in terms of the runtime values it is very powerful. not ideal for what you are talking about though
JameEnder
JameEnderOPā€¢7mo ago
Yeah I can imagine it would be very sweet to have some kind of syntax like this Pure TS
type A = {
age: string
}

type B = {
age: number
}

function AtoB(a: A): B {
return {
age: Number(a.age)
}
}

type C = {
age: BigInt
}

function BtoC(b: B): C {
return {
age: b.age.toBigInt() // or whatever
}
}

const c = BtoC(AtoB({ age: '20' }))
type A = {
age: string
}

type B = {
age: number
}

function AtoB(a: A): B {
return {
age: Number(a.age)
}
}

type C = {
age: BigInt
}

function BtoC(b: B): C {
return {
age: b.age.toBigInt() // or whatever
}
}

const c = BtoC(AtoB({ age: '20' }))
ArkType
const a = type({ age: 'string' })
type A = typeof a.infer

const AtoB = a.map((original) => ({ age: Number(original.age) }))
type B = typeof AtoB.infer

const BtoC = AtoB.map((original) => ({ age: original.age.toBigInt() }))
type C = typeof BtoC.infer

const c = BtoC(AtoB({ age: '20' }))
const a = type({ age: 'string' })
type A = typeof a.infer

const AtoB = a.map((original) => ({ age: Number(original.age) }))
type B = typeof AtoB.infer

const BtoC = AtoB.map((original) => ({ age: original.age.toBigInt() }))
type C = typeof BtoC.infer

const c = BtoC(AtoB({ age: '20' }))
As in this example I'm not duplicating ANY information basically, compared to pure TS approach, where I have to describe the types and the logic too, when the types could be easily derived from the logic itself And also if I could somehow get a type verifier from AtoB or BtoC for free, that would be amazing
ssalbdivad
ssalbdivadā€¢7mo ago
Okay but it really seems like you are talking about piping the values after they've been validated
JameEnder
JameEnderOPā€¢7mo ago
So I could do like
BtoC.plain({ age: 'hello' }) // checks if C type matches correctly
BtoC.plain({ age: 'hello' }) // checks if C type matches correctly
And it would do a normal type check like arktype does
ssalbdivad
ssalbdivadā€¢7mo ago
Not just transforming between types Which is much easier
JameEnder
JameEnderOPā€¢7mo ago
What exactly is the difference there?
ssalbdivad
ssalbdivadā€¢7mo ago
Something like this:
const user = type({
name: "string",
age: "number"
})
const parsedUser = type("string").pipe(s => JSON.parse(s), user)
const user = type({
name: "string",
age: "number"
})
const parsedUser = type("string").pipe(s => JSON.parse(s), user)
You can pass arbitrary transformations to pipe including other types and it will infer/validate them It won't apply them as generics though as that would require them being defined as HKTs
JameEnder
JameEnderOPā€¢7mo ago
const parsedUser = user.pipe(u => ({ deepUser: u }))
typeof parsedUser.infer == { deepUser: typeof user.infer }
const parsedUser = user.pipe(u => ({ deepUser: u }))
typeof parsedUser.infer == { deepUser: typeof user.infer }
Is that legit? Oh wait, no, it uses the validation afterwards in the second argument, hmm
ssalbdivad
ssalbdivadā€¢7mo ago
Right but that is what you expressed in those examples You might need to think through exactly what you want a bit more. But piping is much easier than arbitrary transformations in terms of only the types like mapped types
JameEnder
JameEnderOPā€¢7mo ago
You are right, thanks a lot for the patience, I'll come back to this thread if I come to some conclusions
ssalbdivad
ssalbdivadā€¢7mo ago
No problem this stuff is tricky to think through! Will try to provide the best APIs so your types are as flexible as possible both to transform and to pipe šŸ‘
Dimava
Dimavaā€¢7mo ago
let A = type({ age: 'number' })
let E = type({ bigAge: 'BigInt' })
let AtoE = A.pipe(({age}) => (bigAge: BigInt(age)), E)
let AtoAE = A.pipe(a => ({ ...a, ...E.assert(a) }), [A, '&', E])
let A = type({ age: 'number' })
let E = type({ bigAge: 'BigInt' })
let AtoE = A.pipe(({age}) => (bigAge: BigInt(age)), E)
let AtoAE = A.pipe(a => ({ ...a, ...E.assert(a) }), [A, '&', E])
ssalbdivad
ssalbdivadā€¢7mo ago
It's .pipe in v2 You also can't parse definitions inline with that syntax the way you could with outValidator as the second param So you'd have to define the second param is A.and(E)
Dimava
Dimavaā€¢7mo ago
Is there a generic pipe? (Type<T>._generic((t: Type<T>) => Type<<G<T>>>))?
ssalbdivad
ssalbdivadā€¢7mo ago
No haha, we'll see what APIs I add once I add generics
JameEnder
JameEnderOPā€¢7mo ago
That looks pretty decent Even if it looks very unintuitive, it could get the job done
Dimava
Dimavaā€¢7mo ago
It's possible to make an intuitive wrapper a la
let AtoAE = typeTransformAndMerge(
{ age: 'number' }, // source
{ bigAge: 'BigInt' }, // extend
({age}) => ({bigAge: BigInt(age) }) // callback
)
// : Type<(A) => A&E>
let AtoAE = typeTransformAndMerge(
{ age: 'number' }, // source
{ bigAge: 'BigInt' }, // extend
({age}) => ({bigAge: BigInt(age) }) // callback
)
// : Type<(A) => A&E>
JameEnder
JameEnderOPā€¢7mo ago
I'm more interested in the AtoE function, as the fields are going to be all inherited, either through explicit assignment or return { ...original } But that requires me to define the type E, which is not something I want I would like it to be inherited from the transformation on A itself
Dimava
Dimavaā€¢7mo ago
You have to write the type to validate it Or you don't need validation?
JameEnder
JameEnderOPā€¢7mo ago
Well, I would like it in the future for testing
Dimava
Dimavaā€¢7mo ago
Well, you can
JameEnder
JameEnderOPā€¢7mo ago
But generating a "transform" function and "validation" function at the same time just from inheriting the transformation output would be GOATed
Dimava
Dimavaā€¢7mo ago
Btw you may make it infer E from function and then write the matching definition (with ArkTypo errors)
JameEnder
JameEnderOPā€¢7mo ago
Does that not failt at this problem tho?
Dimava
Dimavaā€¢7mo ago
You have to write the type If you click on function and it suggests "infer return type from definition" you sorta have written it already Hmm Lemme reread the question Yes it fails, you have to write the validation yourself (if you need it) @ssalbdivad eslint --fix rule for type.for<T>() when šŸ˜¹
ssalbdivad
ssalbdivadā€¢7mo ago
What is type.for<T>? To autogenerate the definition from the ts type? I know @TypeHoles was working on somethin glike that for a while
ssalbdivad
ssalbdivadā€¢7mo ago
@JameEnder Wasn't the original question just this?
const parseUserInefficiently = type({
age: type("string").pipe(parseFloat, BigInt)
})

const out = parseUserInefficiently({ age: "5" }) //?

parseUserInefficiently({ age: 5 }).toString() //?
const parseUserInefficiently = type({
age: type("string").pipe(parseFloat, BigInt)
})

const out = parseUserInefficiently({ age: "5" }) //?

parseUserInefficiently({ age: 5 }).toString() //?
No description
Unknown User
Unknown Userā€¢7mo ago
Message Not Public
Sign In & Join Server To View
ssalbdivad
ssalbdivadā€¢7mo ago
Yeah I meant the editor integration for the workbench. Assuming the goal was to get the AT def from the TS type couldn't exactly be done in real-time anyways
JameEnder
JameEnderOPā€¢7mo ago
Oooh, having the fields as pipes is a very interesting idea But sadly the fields dont correspond to the field in the output, and there are some that are created from deeply nested properties of the original
ssalbdivad
ssalbdivadā€¢7mo ago
Even so you could do something very similar at the top-level of the object It is better to have them within a field when you can but if not you just move up a level
JameEnder
JameEnderOPā€¢7mo ago
I think this whole thing is not possible with ArkType either, as it is an issue with the whole Typescript library itself And it's inability to describe function / transform outputs that are recursive
ssalbdivad
ssalbdivadā€¢7mo ago
Yeah it really depends on what you're trying to do haha
JameEnder
JameEnderOPā€¢7mo ago
I really wish I could show you the actual code that i need it for, but NDA :(
ssalbdivad
ssalbdivadā€¢7mo ago
Ahh well
JameEnder
JameEnderOPā€¢7mo ago
I'll try to write up a similar example that doesnt leak anything
ssalbdivad
ssalbdivadā€¢7mo ago
If you're wanting to infer return types of recursive functions then yes in a lot of cases we're stuck with what TS can do But recursive generics work in terms of inference so who knows maybe there are possibilities Especially with HKTs
JameEnder
JameEnderOPā€¢7mo ago
type ApiComment = {
user: {
username: string,
picture: {
uri: string
}
}
body: {
text: string
}
date: {
timestamp: number
}
responses: ApiComment[]
}

function mapApiComment(apiComment: ApiComment) {
return {
username: apiComment.user.username,
content: apiComment.body.text,
date: new Date(apiComment.date.timestamp),
responses: apiComment.responses.map(mapApiComment)
}
}
// ^ this functions returns 'any', because of the recursive weirdness
type ApiComment = {
user: {
username: string,
picture: {
uri: string
}
}
body: {
text: string
}
date: {
timestamp: number
}
responses: ApiComment[]
}

function mapApiComment(apiComment: ApiComment) {
return {
username: apiComment.user.username,
content: apiComment.body.text,
date: new Date(apiComment.date.timestamp),
responses: apiComment.responses.map(mapApiComment)
}
}
// ^ this functions returns 'any', because of the recursive weirdness
ssalbdivad
ssalbdivadā€¢7mo ago
I see. Definitely feels like more of a static analysis issue than a runtime validation one
JameEnder
JameEnderOPā€¢7mo ago
And what I expected TS/AT to do is to infer the type as
type Comment = {
username: ApiComment['user']['username'],
content: ApiComment['body']['text']
date: Date
responses: Comment[]
}
type Comment = {
username: ApiComment['user']['username'],
content: ApiComment['body']['text']
date: Date
responses: Comment[]
}
ssalbdivad
ssalbdivadā€¢7mo ago
It would have to create an anonymous type for that recursive result But yeah
JameEnder
JameEnderOPā€¢7mo ago
Yeah, most likely, I mean runtime validation was a thing I wanted to do also, but now that I'm thinking about it, if its not possible in TS itself, it wont be possible with AT
ssalbdivad
ssalbdivadā€¢7mo ago
It's pretty easy to just adapt the type to what you need and type the return explicitly at least
JameEnder
JameEnderOPā€¢7mo ago
Not when there is like 30 fields :D I mean its still not like difficult It's just annoying having to repeat myself, when the compiler already knows every single type there And if I wanted to have the type be able to adapt to the input field types changing, I would have to use this insane syntax for it Which I don't think I will, as there are like deeply nested turnaries and ugh But thanks a lot for your help! <3
ssalbdivad
ssalbdivadā€¢7mo ago
Good luck!
Dimava
Dimavaā€¢7mo ago
Hmm, so you need JSON restructuring?
JameEnder
JameEnderOPā€¢7mo ago
I mean, I need object restructuring :D With ability to infer recursive return types of a function taht does that "restructure"
ssalbdivad
ssalbdivadā€¢7mo ago
I think in most cases like this you would want to have an explicit type for the return- it would be weird to have that function be the source of truth for a type like that
Dimava
Dimavaā€¢7mo ago
Restructuring is a COMPLETE DIFFERENT PROBLEM ā„¢ļø šŸ¤”
ssalbdivad
ssalbdivadā€¢7mo ago
Also because at best TS could say:
{
username: ApiComment['user']['username'],
content: ApiComment['body']['text']
date: Date
responses: ReturnType<typeof mapApiComment>
}
{
username: ApiComment['user']['username'],
content: ApiComment['body']['text']
date: Date
responses: ReturnType<typeof mapApiComment>
}
Dimava
Dimavaā€¢7mo ago
* from validation
ssalbdivad
ssalbdivadā€¢7mo ago
I mean it's kind ofa nothing problem for the most part any abstraction you make beyond just having a pipe API that allows you to reconstruct the object using normal syntax is going to be a mess Well not a mess But just needless complexity for something that can already be expressed with the language
Dimava
Dimavaā€¢7mo ago
JSON I mean as a "plain js object" here (w/o classes dates and Maps)
JameEnder
JameEnderOPā€¢7mo ago
I mean, couldnt it just create an anonymous recursive type and include it in the definition? Got you
ssalbdivad
ssalbdivadā€¢7mo ago
Right or that. Referencing the return type would be at least a little clearer there IMO than ... or notoriously if you have noErrorTruncation on it shows up as any But I'm just saying that type really isn't defined anywhere else you just construct it there from that function and then reference it elsewhere?
Dimava
Dimavaā€¢7mo ago
@ssalbdivad can you use this for superobject in inline morph? (i don't have an idea I have a feeling)
ssalbdivad
ssalbdivadā€¢7mo ago
I dunno either but this won't work until generics work
Dimava
Dimavaā€¢7mo ago
@JameEnder I can imagine remap but this is hard lol
ssalbdivad
ssalbdivadā€¢7mo ago
The only advantage would be that we'd have the runtime definition of the output type I'm very confident whatever other custom syntax you'd create to do that would just be more confusing than mapping/mutating the object with a pipe
Dimava
Dimavaā€¢7mo ago
let r = remap(A)
let AtoE = remap({
age: r.age,
bigAge: r.age.morph(BigInt)
})
let r = remap(A)
let AtoE = remap({
age: r.age,
bigAge: r.age.morph(BigInt)
})
ssalbdivad
ssalbdivadā€¢7mo ago
That's not remap at all that's just index access on a type definition And remap is just type
Dimava
Dimavaā€¢7mo ago
I'm imagining fn-like api for JSON restructuring
ssalbdivad
ssalbdivadā€¢7mo ago
Well you do you I'm just predicting that would turn out badly
JameEnder
JameEnderOPā€¢7mo ago
I mean this would be a completely fine definition by me And I find it better than just throwing "any" out of the function
ssalbdivad
ssalbdivadā€¢7mo ago
It's an error though they just require you to explicitly type it Yeah I agree inferring something would be better but I get why it's not a top priority usually you'd want to have a type like that defined somewhere anyways
PIat
PIatā€¢4mo ago
That's so cool šŸ˜©
Want results from more Discord servers?
Add your server