errata
errata
Aarktype
Created by errata on 9/11/2024 in #questions
extracting redux-like actions from type.enumerated discriminated union
Trying unsuccessfully to convert some redux/reducer-like "action" object types to arktype:
type TestAction1 = { type: 'ACTION_1', value: number }
type TestAction2 = { type: 'ACTION_2', name: string}
type TestAction = TestAction1 | TestAction2

function CreateTestAction1(): Extract<TestAction, {type: 'ACTION_1'}> {
return { type: 'ACTION_1', value: 3 }
}

const testAction1 = CreateTestAction1() // type === TestAction1
type TestAction1 = { type: 'ACTION_1', value: number }
type TestAction2 = { type: 'ACTION_2', name: string}
type TestAction = TestAction1 | TestAction2

function CreateTestAction1(): Extract<TestAction, {type: 'ACTION_1'}> {
return { type: 'ACTION_1', value: 3 }
}

const testAction1 = CreateTestAction1() // type === TestAction1
Below is how I did the conversion. The Extract on the type.enumerated results in a never and I don't follow why.
const ArkAction1 = type({ type: type.unit("ACTION_1"), value: type.number })
const ArkAction2 = type({ type: type.unit("ACTION_2"), name: type.string })
const ArkAction = type.enumerated(ArkAction1, ArkAction2)
type ArkAction = type.infer<typeof ArkAction>

function CreateArkAction1(): Extract<ArkAction, { type: 'ACTION_1'}> {
return { type: 'ACTION_1', value: 3 } // type not assignable to never
}
const ArkAction1 = type({ type: type.unit("ACTION_1"), value: type.number })
const ArkAction2 = type({ type: type.unit("ACTION_2"), name: type.string })
const ArkAction = type.enumerated(ArkAction1, ArkAction2)
type ArkAction = type.infer<typeof ArkAction>

function CreateArkAction1(): Extract<ArkAction, { type: 'ACTION_1'}> {
return { type: 'ACTION_1', value: 3 } // type not assignable to never
}
If I define the action union type in vanilla TS (from the ark inferred types) the extract works as expected:
type ArkAction1 = type.infer<typeof ArkAction1>
type ArkAction2 = type.infer<typeof ArkAction2>
type ArkAction = ArkAction1 | ArkAction2
type ArkAction1 = type.infer<typeof ArkAction1>
type ArkAction2 = type.infer<typeof ArkAction2>
type ArkAction = ArkAction1 | ArkAction2
Am I holding it wrong?
77 replies
Aarktype
Created by errata on 9/10/2024 in #questions
alternative to nesting quotes in quotes for strings?
const Target = type("'BODY' | 'FACE' | 'ARMS'")

const Entity = type({
targets: Target.array()
})
const EntityDefn = generic(['T', Entity])('T')
const Creature = EntityDefn({
targets: ["'BODY'", "'FACE'"]
})
const Target = type("'BODY' | 'FACE' | 'ARMS'")

const Entity = type({
targets: Target.array()
})
const EntityDefn = generic(['T', Entity])('T')
const Creature = EntityDefn({
targets: ["'BODY'", "'FACE'"]
})
Needing to double + single quote everything feels really fiddly and clumsy, like, I feel like I don't my coworkers to see this code lest they judge me. Is there a cleaner, alternative syntax/api?
24 replies
Aarktype
Created by errata on 9/10/2024 in #questions
template literal/backtick string types e.g. type T = `type-${string}`
I can't see how to do in Ark:
type UserId = `user-${string}`
type UserId = `user-${string}`
I specifically want compile-time checking. Tried to drop it in "1:1" style, but this doesn't work as-is:
const UserId = type('`user-${string}`') // doesn't work
const UserId = type('`user-${string}`') // doesn't work
nor did it work trying to use 'type' as a tagged template:
const UserId = type`user-${type('string')}`
const UserId = type`user-${type('string')}`
I did look in the docs but I don't believe it's there yet, and if it's in the tests I didn't spot it Do I have to set it up as a custom string constraint so it'd be like const UserId = type('string-userId')? IIUC this doesn't get the compile-time checking:
// Subtypes like 'email' are inferred like 'string' but provide additional validation at runtime. email: "string.email",
14 replies
Aarktype
Created by errata on 9/9/2024 in #questions
Compile-time typesafe merge/and for data-only "traits"
I've evaluating introducing arktype to a typescript codebase, but I got stuck trying to use it to set up my POJO "traits" system. This is similar in concept to the Trait abstract class in arktype/util but this is data only; so far I've avoided the need to introduce any classes and I'm hoping to maintain this approach. So I have a discriminated union that represents the different "traits" and the union members share a common shape (mainly references to other trait names), something along these lines:
type TraitName = 'TraitCanScream' | 'TraitHasMouth' // ideally this would be derived e.g. AnyTrait['type']
type Trait = {
requires: TraitName[] // trait can require other traits e.g. scream requires mouth
}

type TraitDefn<T extends Trait> = Trait & T

type TraitHasMouth = TraitDefn<{
type: 'TraitHasMouth'
requires: []
}>

type TraitCanScream = TraitDefn<{
type: 'TraitCanScream'
requires: ['TraitHasMouth'] // if object has 'TraitCanScream', it must also have 'TraitHasMouth'
}>

// @ts-expect-error
type TraitMistake = TraitDefn<{
type: 'TraitMistake'
requires: ['KJAHBSDKAJSB'] // should catch that this isn't a valid type
}>

// discriminated union here
type AnyTrait = TraitCanScream | TraitHasMouth

// an object with traits would have them in an object keyed by TraitName
// i.e. stricter Record<TraitName, AnyTrait>
type Traits = {
TraitHasMouth: TraitHasMouth
TraitCanScream: TraitCanScream
}

type TraitsObject = Partial<Traits>

function HasTrait<T extends TraitName>(traitName: T, traits: TraitsObject): traits is Pick<Traits, T> {
return traitName in traits && traits[traitName]?.type === traitName
}

// this is what I want to assert: if an object has a trait, it also has all of its required traits (recursively)
type RequiredTraits<T extends TraitName> = Pick<Traits, T | Traits[T]['requires'][number]>

// ensures traits has all required traits
function HasRequiredTraits<T extends TraitName>(traitName: T, traits: TraitsObject): traits is RequiredTraits<T> {
if (!HasTrait(traitName, traits)) return false
const trait = traits[traitName]
for (const requiredTrait of trait.requires) {
if (!HasRequiredTraits(requiredTrait, traits)) return false
}
return true
}
type TraitName = 'TraitCanScream' | 'TraitHasMouth' // ideally this would be derived e.g. AnyTrait['type']
type Trait = {
requires: TraitName[] // trait can require other traits e.g. scream requires mouth
}

type TraitDefn<T extends Trait> = Trait & T

type TraitHasMouth = TraitDefn<{
type: 'TraitHasMouth'
requires: []
}>

type TraitCanScream = TraitDefn<{
type: 'TraitCanScream'
requires: ['TraitHasMouth'] // if object has 'TraitCanScream', it must also have 'TraitHasMouth'
}>

// @ts-expect-error
type TraitMistake = TraitDefn<{
type: 'TraitMistake'
requires: ['KJAHBSDKAJSB'] // should catch that this isn't a valid type
}>

// discriminated union here
type AnyTrait = TraitCanScream | TraitHasMouth

// an object with traits would have them in an object keyed by TraitName
// i.e. stricter Record<TraitName, AnyTrait>
type Traits = {
TraitHasMouth: TraitHasMouth
TraitCanScream: TraitCanScream
}

type TraitsObject = Partial<Traits>

function HasTrait<T extends TraitName>(traitName: T, traits: TraitsObject): traits is Pick<Traits, T> {
return traitName in traits && traits[traitName]?.type === traitName
}

// this is what I want to assert: if an object has a trait, it also has all of its required traits (recursively)
type RequiredTraits<T extends TraitName> = Pick<Traits, T | Traits[T]['requires'][number]>

// ensures traits has all required traits
function HasRequiredTraits<T extends TraitName>(traitName: T, traits: TraitsObject): traits is RequiredTraits<T> {
if (!HasTrait(traitName, traits)) return false
const trait = traits[traitName]
for (const requiredTrait of trait.requires) {
if (!HasRequiredTraits(requiredTrait, traits)) return false
}
return true
}
I'm confused about how I would represent this setup in arktype in a typesafe manner? Or would you suggest an alternative way of representing this? I don't have any significant code written against these types so I'm open to rethinking this setup entirely. I got stuck almost immediately as neither .and(...) nor.merge(...) will allow me to inherit compile-time type checking of requires in my Trait subtypes. Please help! Thanks
38 replies