用于删除特定嵌套路径处的对象的类型

Abh*_*aha 0 typescript typescript-generics

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;
Run Code Online (Sandbox Code Playgroud)

我有上面的类型,效果很好。我已经这样测试过:

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

// {
//    a: {
//        b: Pick<{
//            c: 1;
//        }, never>;
//    };
// }
Run Code Online (Sandbox Code Playgroud)

但是,当我将其添加到函数中时,我没有得到所需的输出。

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
Run Code Online (Sandbox Code Playgroud)

有人能指出我这里有什么问题吗?

jca*_*alz 5

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

如果您希望将数组类型推断为元组(如果可能),则应在其上下文中包含元组类型,例如通过联合。于是就T extends Foo[]变成了T extends Foo[] | []。如果您希望将类型推断为字符串文字(如果可能),则应在其上下文中或其他string类似提示中包含字符串文字。所以T extends string[]变成要么T extends (string | (""&{__: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>;
Run Code Online (Sandbox Code Playgroud)

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

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

另请注意,您的定义DeepOmit不必要地迂回;当编译器认为是循环时,该{0: X, 1: Y}[T extends U ? 0 : 1]技术是一种规避错误的方法(即使不支持T extends U ? X : Y这种回避)。但直接实现并不违反这些规则:有一个递归类型很好,其中递归发生在对象属性中: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
Run Code Online (Sandbox Code Playgroud)

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

Playground 代码链接