Bob
Bob

Reputation: 1597

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.

Upvotes: 0

Views: 566

Answers (2)

ChildAttribute should return a union of all allowed values.

type RemoveDollar<
  T extends string,
  Result extends string = ''
  > =
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends ''
      ? (Head extends '$' ? Result : `${Result}${Head}`) : RemoveDollar<Rest, `${Result}${Head}`>) : never
  )

// owner
type Test = RemoveDollar<'owner$'>

As far as I understand, if value is with $ we can apply short getter but if property is without $ like name - we can't use name$ as a getter.

If my assumption is correct this solution should work for you:

interface Pet {
  name: string;
  owner$: any;
}


type RemoveDollar<
  T extends string,
  Result extends string = ''
  > =
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends ''
      ? (Head extends '$' ? Result : `${Result}${Head}`) : RemoveDollar<Rest, `${Result}${Head}`>) : never
  )
// owner
type Test = RemoveDollar<'owner$'>

type WithDollar<T extends string> = T extends `${string}\$` ? T : never

// owner$
type Test2 = WithDollar<keyof Pet>

type ChildAttribute<E> = keyof E extends string ? RemoveDollar<keyof E> | WithDollar<keyof E> : never

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

Playground

RECURSION



type RemoveDollar<
  T extends string,
  Result extends string = ''
  > =
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends ''
      ? (Head extends '$' ? Result : `${Result}${Head}`) : RemoveDollar<Rest, `${Result}${Head}`>) : never
  )

/**
 * First cycle
 */

type Call = RemoveDollar<'foo$'>

type First<
  T extends string,
  Result extends string = ''
  > =
  // T extends        `{f}         {oo$}
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends '' // Rest is not empty
      // This branch is skipped on first iteration         
      ? (Head extends '$' ? Result : `${Result}${Head}`)
      // RemoveDollar<'oo$', ${''}${f}>
      : RemoveDollar<Rest, `${Result}${Head}`>
    ) : never
  )

/**
 * Second cycle
 */
type Second<
  T extends string,
  // Result is f
  Result extends string = ''
  > =
  // T extends       `{o}$         {o$}
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends '' // Rest is not empty
      // This branch is skipped on second iteration         
      ? (Head extends '$' ? Result : `${Result}${Head}`)
      // RemoveDollar<'o$', ${'f'}${o}>
      : RemoveDollar<Rest, `${Result}${Head}`>
    ) : never
  )

/**
* Third cycle
*/
type Third<
  T extends string,
  // Result is fo
  Result extends string = ''
  > =
  // T extends       `{o}          {$}
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends '' // Rest is not empty, it is $
      // This branch is skipped on third iteration         
      ? (Head extends '$' ? Result : `${Result}${Head}`)
      // RemoveDollar<'$', ${'fo'}${o}>
      : RemoveDollar<Rest, `${Result}${Head}`>
    ) : never
  )

/**
* Fourth cycle, the last one
*/
type Fourth<
  T extends string,
  // Result is foo
  Result extends string = ''
  > =
  // T extends       `${$}        {''}
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends '' // Rest is  empty
      // Head is $           foo   
      ? (Head extends '$' ? Result : `${Result}${Head}`)
      // This branch is skipped on last iteration
      : RemoveDollar<Rest, `${Result}${Head}`>
    ) : never
  )

Playground

Upvotes: 1

Jean-Alphonse
Jean-Alphonse

Reputation: 816

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"

Upvotes: 1

Related Questions