首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >缩小所有的subTree

缩小所有的subTree
EN

Stack Overflow用户
提问于 2021-05-18 15:58:49
回答 1查看 63关注 0票数 1

这与使用3Plib中的类型有关。

我希望知道是否有一种快速的方法来缩小所有已知的子树,这样我就不需要单独地断言每个字段和子字段了?

代码语言:javascript
复制
interface User {
  id: string;
  private?: Private;
}
interface Private {
  info?: Info;
  images?: ImageData[];
  // ...
}
interface Info {
  name?: string;
  foo?: Foo;
}
// etc

async function client_list(id: string, parts: string[]) {
  // retrieve some user data
  // if successful, is guaranteed to return a known tree
  // eg for parts == ["info"]
  const user = {
    id: id,
    private: {
      info: {
        foo: {
          bar: {
            some_number: 42
          }
        }
      }
    }
  } as User;
  return user;
}

(async () => {
  const user = await client_list("abcd", ["info"]);

  user.private.info.foo.bar.some_number; // private, info, foo and bar are possibly undefined
  // after somehow narrowing the subtree down to some_mumber, asserting each node is not undefined
  user.private.info.foo.bar.some_number; // everything would be fine
})();

这里的工作示例

提前谢谢你。

Ps:我认为复制接口但没有可选属性会很好,但这将意味着复制大量的接口。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2021-05-19 02:29:05

您可以使用递归映射有条件的来获取对象类型T和对应于该对象的索引路径的元组 KS,并将其转换为一个新类型,在该类型中,路径KS处的T子树被替换为其自身的一个版本,在该版本中,所有属性和子属性都是必需的,而不是可选的。

但它并不特别漂亮。

如果您想要做的只是将T转换成一个版本,其中所有的属性、子属性、子属性等等都是必需的,而不是可选的,那么您可以很容易地做到这一点:

代码语言:javascript
复制
type DeepRequired<T> =
  T extends Function ? T : { [K in keyof T]-?: DeepRequired<T[K]> }

在这里,我们将函数单独放在一边(映射类型对函数没有好处,所以最好不要修改它们);对于非函数类型,我们只是使用-? 映射类型修饰符将任何可能的可选属性转换为所需的属性,而我们则向下递归。

让我们看看这对您的一个接口造成了什么影响:

代码语言:javascript
复制
type DeepRequiredInfo = DeepRequired<Info>
/* type DeepRequiredInfo = {
    name: string;
    foo: {
        bar: {
            some_number: number;
        };
    };
} */

您可以看到,namefoo属性DeepRequired<Info>都是必需的。name的类型是string,而foo的类型是DeepRequired<Foo>,它的属性是必需的,依此类推。

注意,DeepRequired<ImageData>也会向下遍历它的属性,使它们都是必需的,这可能不是您想要的。但我会考虑在这个问题的范围之外做任何不同的事情。就目前而言,这只是一个潜在的警告。

当然,DeepRequired并不完全是您想要的;您想要的是类似于RequireSubtree<User, ["private", "info"]>这样的东西,其中只有Userprivate属性是必需的,只有private属性的info属性是必需的,而且该属性的类型是‘`DeepRequired’。

好吧,这就是我们该怎么做的。首先,编写一个名为Expand<T>的类型函数很有用,它接受像{a: string} & {b: number}这样的对象类型的丑陋的交叉口,并将它们转换为{a: string; b: number}等等效的单个对象类型,并且倾向于将嵌套类型函数(如Baz<Qux<Fnord>> )转换为具有明确写入属性的等效对象类型;有关更多信息,请参见这个问题

代码语言:javascript
复制
type Expand<T> = T extends infer U ? { [K in keyof U]: T[K] } : never;

现在,有趣的是:

代码语言:javascript
复制
type RequireSubtree<T, KS extends readonly any[]> =
  T extends Function ? (
    T
  ) : (
    KS extends readonly [infer K, ...infer R] ? (
      [K] extends [never] ? T :
      [K] extends [keyof T] ? (
        Expand<
          Omit<T, K> &
          { [P in K]-?: Exclude<RequireSubtree<T[P], R>, undefined> }
        >
      ) : (
        T
      )
    ) : (
      DeepRequired<T>
    )
  );

同样,如果T是一个函数类型,我们不想改变它。

然后我们来看一下元组KS的路径。如果它不是空的,我们使用它的第一个元素K,并保留其余的元组R。如果KT的一个键,那么我们希望使用没有K子树的T (使用实用程序类型),并将其与一个版本的K子树交叉,该子树的属性键都是必需的,其属性值从它们中得到undefined D,其属性值已通过RequireSubTree递归地使用路径元组R运行。

如果路径元组是空的,那么我们最终会沿着完整的路径走下去,我们可以返回DeepRequired<T>

有一些奇怪的边缘案件正在处理中,我不会进入,而且可能有更奇怪的边缘案件,没有处理,我也不会进入。所以有大量的警告。

但现在让我们确保它能做正确的事情:

代码语言:javascript
复制
type UserWithDeepRequiredPrivateInfo = RequireSubtree<User, ["private", "info"]>
/* type UserWithDeepRequiredPrivateInfo = {
    id: string;
    private: {
        images?: ImageData[] | undefined;
        info: {
            name: string;
            foo: {
                bar: {
                    some_number: number;
                };
            };
        };
    };
} */

这看起来很好;private属性是必需的,它的类型是一个非常需要的info子树,但是images属性仍然是可选的,这是需要的。

现在,我们可以给您的client_list函数提供一个类型签名:

代码语言:javascript
复制
async function client_list<K extends keyof Private>(id: string, parts: K[]):
  Promise<RequireSubtree<User, ["private", K]>> {
  throw new Error("needs to be properly implemented");
}

parts列表似乎是Private的键数组,而client_list的返回类型是Promise of RequireSubtree<User, ["private", K]>>。请注意,正确地实现此函数需要注意,因为编译器不可能验证为泛型Promise<RequireSubtree<User, ["private", K]>>分配的任何内容;您将需要一个类型断言或类似的东西来抑制编译器错误。

无论如何,我们终于可以调用client_list,看看它是如何工作的:

代码语言:javascript
复制
const user = await client_list("abcd", ["info"]);
/* const user: const user: {
  id: string;
  private: {
      images?: ImageData[] | undefined;
      info: {
          name: string;
          foo: {
              bar: {
                  some_number: number;
              };
          };
      };
  };
 } */
user.private.info.foo.bar.some_number; // okay
user.private.images.map(iD => iD.height.toFixed(2)); // error, possibly undefined

这看起来很好;info属性有一个完全必需的子树,而images属性仍然可能是undefined

将其与以下内容进行比较:

代码语言:javascript
复制
const otherUser = await client_list("efgh", ["images"]);
otherUser.private.info.foo.bar.some_number; // error, possibly undefined
otherUser.private.images.map(iD => iD.height.toFixed(2)); // okay

const thirdUser = await client_list("ijkl", ["images", "info"]);
thirdUser.private.info.foo.bar.some_number; // okay
thirdUser.private.images.map(iD => iD.height.toFixed(2)); // okay

const nilUser = await client_list("mnop", []);
nilUser.private.info.foo.bar.some_number; // error, possibly undefined
nilUser.private.images.map(iD => iD.height.toFixed(2)); // error, possibly undefined

我觉得这是你想要的行为,对吧?

注意,如果我认为您只会使用一个或两个层次深的子树路径,那么我可能不会建议使用一个完全递归的RequiredSubtree。相反,我可能只会使用DeepRequired和一些privateinfo的手工输入。但我假设这些只是示例,您的实际用例可能在某些任意深度上具有所需的子树。

最后,您应该考虑完全递归映射的条件类型是否值得这么复杂;您刚才提到的手动版本很乏味,但概念上很简单;任何查看您的自定义接口的人都可能能够理解他们做什么,以及在出了问题时如何调试它,而有些人则在查看RequiredSubtree<Foo, ["bar", "baz", "qux"]>,并试图找出为什么在一段艰难的时间内它并不是他们所期望的那样。

操场链接到代码

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/67589864

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档