【问题标题】:Typescript template literals with inference带有推理的打字稿模板文字
【发布时间】:2021-09-09 17:16:50
【问题描述】:

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

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

什么是合适的类型才能使它工作?

--- 编辑

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

【问题讨论】:

    标签: typescript template-literals


    【解决方案1】:

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

    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"
    

    【讨论】:

    • 简洁明了!为了获得预期的结果(我编辑了问题以进行澄清),ChildAttribute 定义中的结尾“K”应该是“从不”,因此“名称”不会是它的可能值。可以请编辑吗?
    【解决方案2】:

    ChildAttribute 应该返回所有允许值的联合。

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

    据我了解,如果 value 带有 $,我们可以应用短 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

    【讨论】:

    • 递归类型,哇!但是T extends `${infer Head}${infer Rest}` 中的推理是如何工作的?例如,'owner$' 的第一个循环中的 Head and Rest 是什么? (另外,感谢您的快速回答!为了简洁起见,我会接受@Jean-Alphonse 的回答,但一定会注意您的)
    • @Bob 进行了更新。好没问题。我的似乎是过度设计的
    • 好的,默认情况下,第一个推理采用第一个字符。感谢您花时间解释!
    猜你喜欢
    • 2023-01-11
    • 1970-01-01
    • 2018-06-09
    • 2019-10-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多