arktypea
arktype16mo ago
errata

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
}

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
Was this page helpful?