【问题标题】:Types for deleting object at specific nested path用于在特定嵌套路径中删除对象的类型
【发布时间】:2020-06-17 03:30:54
【问题描述】:
type Tail<T extends any[]> = ((...t: T) => void) extends ((
  h: any,
  ...r: infer R
) => void)
  ? R
  : never;

type DeepOmit<T, Path extends string[]> = T extends object
  ? {
      0: Omit<T, Path[0]>;
      1: {
        [K in keyof T]: K extends Path[0] ? DeepOmit<T[K], Tail<Path>> : T[K];
      };
    }[Path['length'] extends 1 ? 0 : 1]
  : T;

我有上述类型,效果很好。我是这样测试的:

type A = DeepOmit<{ a: { b: { c: 1 } } }, ['a', 'b', 'c']>;

// {
//    a: {
//        b: Pick<{
//            c: 1;
//        }, never>;
//    };
// }

但是,当我在函数中添加它时,我没有得到想要的输出。

const del = <T extends object, K extends string[]>(src: T, path: K) => {
  // custom logic removed. just creating the result below.
  const result = {
    a: {
      b: {}
    }
  }
  return result as DeepOmit<T, K>
};

const deletedObj = del({a:{b:{c:1}}},["a","b","c"]);
// deletedObj.a.b.c <========== still works
// expected is deletedObj.a.b

谁能指出这里有什么问题?

【问题讨论】:

    标签: typescript typescript-generics


    【解决方案1】:

    您面临的问题是编译器倾向于将元组扩展为数组,将文字扩展为非文字,除非您给出特定提示。 del()调用者 可以在path 参数上使用as const 断言来获得这种效果(尽管您需要接受readonly string[] 而不仅仅是string[]),但是您大概希望呼叫者不要担心它。我已经提交microsoft/TypeScript#30680 要求在呼叫签名上提供类似as const 的内容来执行此操作。在没有这样的功能的情况下,您必须依靠一些奇怪的提示。

    如果您希望尽可能将数组类型推断为元组,则应在其上下文中包含元组类型,例如通过联合。所以T extends Foo[] 变成了T extends Foo[] | []。如果您希望尽可能将类型推断为字符串文字,则应在其上下文中包含字符串文字或其他类似string 的提示。所以T extends string[] 变成T extends (string | (""&amp;{__:0}))[] 或者,我的偏好是T extends N[],其中N 是另一个通用参数N extends string。下面是它的外观:

    declare const del: <T extends object, K extends N[] | [], N extends string>(
        src: T, path: K
    ) => DeepOmit<T, K>;
    

    您可以看到它按预期工作:

    const deletedObj = del({ a: { b: { c: 1 } } }, ["a", "b", "c"]);
    deletedObj.a.b; // okay
    deletedObj.a.b.c; // error
    

    还请注意,您对DeepOmit 的定义是不必要的迂回;当编译器认为T extends U ? X : Y 是循环的(即使这种回避是not supported)时,{0: X, 1: Y}[T extends U ? 0 : 1] 技术是一种规避错误的方法。但是DeepOmit 的直接实现并没有违反这些规则:可以有一个递归类型,其中递归发生在对象属性中:

    type DeepOmit<T, Path extends string[]> = T extends object ?
        Path['length'] extends 1 ? Omit<T, Path[0]> : {
            [K in keyof T]: K extends Path[0] ? DeepOmit<T[K], Tail<Path>> : T[K];
        } : T; // no error, still works
    

    好的,希望对您有所帮助;祝你好运!

    Playground link to code

    【讨论】:

    • 那是正确的。我忘了我们可以取出元素类型,推理会变得更好。
    • @jcalz 谢谢你的回答。我面临的问题是所有方法都在接口内。我理解了你描述的问题并去 github 阅读了更多关于它的信息。由于我不能在接口内声明 const,他们还有其他方法可以实现吗?
    【解决方案2】:

    该问题与第二个参数的类型推断有关。在类型示例中,类型严格为['a','b','c'],但在函数参数中被推断为string[],因此您的类型级别函数DeepOmit 使用string[] 调用,而不是['a','b','c']。结果是一样的:

    type Result = DeepOmit<{ a: { b: { c: 1 } } }, string[]>;
    

    为了解决这个问题,我们需要或严格输入变量['a','b','c'] as ['a','b','c'],这非常冗长,或者我们可以通过分别处理每个参数(使用spread)来使函数更严格。缺点是函数的 API 会发生一些变化(我会说更好?),但推理现在按预期工作。考虑:

    const del = <T extends object, K extends string[]>(src: T, ...path: K) => {
      // custom logic removed. just creating the result below.
      const result = {
        a: {
          b: {}
        }
      }
      return result as DeepOmit<T, K>
    };
    
    const deletedObj = del({ a: { b: { c: 1 } } }, 'a', 'b', 'c');
    deletedObj.a.b.c // error no c !
    

    【讨论】:

    • 感谢 Maciej。这很好用,但是,我不能更改 API。有多种方法具有相似的签名,所以不想为这个方法做一个例外。
    猜你喜欢
    • 2021-11-17
    • 1970-01-01
    • 1970-01-01
    • 2019-11-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多