The way zod and arktype generally handle this is by providing treating the schema as the source of truth, rather than a type. They then provide a way to define the type in terms of the schema:
// zod 3 syntax
import { z } from 'zod'
const RGB = z.schema({
red: z.number(),
green: z.number(),
blue: z.number(),
})
type RGB = z.infer<typeof RGB>
// same thing as:
// type RGB = { red: number; green: number; blue: number };
For the initial migration, there are tools that can automatically convert types into the equivalent schema. A quick search turned up https://transform.tools/typesgcript-to-zodghl, but I've seen others too.
For what it's worth, I have come to prefer this deriving types from parsers approach to the other way around.
With Zod you can build a schema that would match an existing type. Typescript will complain if the schema you build does not match the type you are representing, which is helpful. From memory:
import { z } from ‘zod’
type Message = { body: string; }
const messageSchema: z.Type<Message> = z.object({ body: z.string() })
For what it's worth, I have come to prefer this deriving types from parsers approach to the other way around.