Search code examples
typescripttemplate-literals

Typescript template literals with inference


A code convention marks the child of an entity (= an association with another entity) with a '$'.

class Pet {
  owner$: any;
}

When referring to an entity child, the user should be allowed to use the full form ('owner$') or a simpler form ('owner').

I'm trying such construct:

type ChildAttributeString = `${string}\$`;
type ShortChildAttribute<E> = ((keyof E) extends `${infer Att}\$` ? Att : never);
type ChildAttribute<E> = (keyof E & ChildAttributeString) | ShortChildAttribute<E>;

const att1: ChildAttribute<Pet> = 'owner$'; // Valid type matching
const att2: ChildAttribute<Pet> = 'owner'; // Valid type matching
const att3: ChildAttribute<Pet> = 'previousOwner$'; // Invalid: previousOwner$ is not an attribute of Pet - Good, this is expected

This works as long as ALL attributes of Pet are child attributes, but as soon as we add a non-child attribute, the matching breaks:

class Pet {
  name: string;
  owner$: any;
}
const att1: ChildAttribute<Pet> = 'owner$'; // Valid type matching
const att2: ChildAttribute<Pet> = 'owner'; // INVALID: Type 'string' is not assignable to type 'never'
// To be clear: ChildAttribute<Pet> should be able to have these values: 'owner', 'owner$'
// but not 'name' which is not a child (no child indication trailing '$')

What would be the proper types to make that work ?

--- edit

I haven't been clear on the expected result and the definition of an "entity child", hence the posted answers, so I edited the question to make it clearer.


Solution

  • Here we map over the keys: if a key ends with $, we include both full and short forms, otherwise we omit it:

    type ValuesOf<T> = T[keyof T]
    type ChildAttribute<E> = 
      ValuesOf<{ [K in keyof E]: K extends `${infer Att}$` ? K | Att : never }>
    
    interface Pet {
        name: string
        owner$: any
    }
    
    type PetAttr = ChildAttribute<Pet> // "owner$" | "owner"