带有推理的打字稿模板文字

Typescript template literals with inference

代码约定用“$”标记实体的子项(= 与另一个实体的关联)。

class Pet {
  owner$: any;
}

在引用子实体时,应允许用户使用完整形式 ('owner$') 或更简单的形式 ('owner')。

我正在尝试这样的构造:

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

只要 Pet 的所有属性都是子属性,这就有效,但是一旦我们添加非子属性,匹配就会中断:

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 '$')

使该工作正常进行的正确类型是什么?

---编辑

我不清楚预期结果和“实体子项”的定义,因此发布了答案,所以我编辑了问题以使其更清楚。

ChildAttribute 应该 return 所有允许值的并集。

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$'>

据我了解,如果值带有 $ 我们可以应用短 getter 但如果 属性 没有 $ 就像 name -我们不能将 name$ 用作 getter.

如果我的假设是正确的,这个解决方案应该适合你:

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

递归



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

这里我们映射键:如果键以 $ 结尾,我们包括完整形式和简短形式,否则我们忽略它:

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"