假设我有一个带有键和值的动态对象.密钥可以是包括nullundefined在内的任何类型.

现在假设我有一个函数,它获取指向所述对象中的值的字符串路径数组,如果它们都存在(不是nullundefined),则返回true.

我怎样才能让Typescript 推断出这一信息,这样我就不需要使用!运算符:

interface Foo {
  a?: { i?: number; ii?: number },
  b?: string;
  c?: {}
}
var object: Foo = {
    a: {
        i: undefined,
        ii: 1
    },
    b: undefined,
    c: {}
}

if (exists(object, ["a.ii", "c"])) // returns true if `object.a.ii` and `object.c` is not `null` or `undefined`
{
    // get typescript to infer that `object.a.ii` is not `null` or `undefined`
    // so that I can ommit `!` operator
    let value: number = object.a.ii!;
}

我知道你可以这样做:

function exists<T>(object: T | null | undefined): object is T
{
    return object !== null && object !== undefined;
}

if (exists(object.a.ii) && exists(object.c))
{
    let value: number = object.a.ii; // works fine without '!' operator
}

但我想知道是否可以使用字符串路径.

编辑:将类型添加到示例

推荐答案

我们希望exists(obj, paths)函数是generic-user-defined type guard function.如果obj是泛型类型T,paths是泛型类型K[],那么我们希望true返回值将obj的范围缩小到T的子类型,这些子类型已知在由K表示的路径上具有非nullish属性.

这意味着我们需要编写一个the Record<K, V> utility type的"嵌套"版本,将K视为由点路径组成的union,而不是普通键的联合.我们就叫它NestedRecord<K, V>吧.我们希望它是这样的:

type Example = NestedRecord<
  "z.y.x" | "z.w.v" | "u.t" | "u.t.s" | "r.q" | "p",
  Date
>;
/* type Example = {
  z: {
    y: { x: Date; };
    w: { v: Date; };
  };
  u: { t: Date & { s: Date; }; };
  r: { q: Date; };
  p: Date;
}*/

由于非空值正好对应于"空对象类型"{}(请参见How to undestand relations between types any, unknown, {} and between them and other types?),因此我们希望exists()的调用签名如下所示:

declare function exists<T extends object, K extends string>(
  obj: T, paths: K[]
): obj is T & NestedRecord<K, {}>;

所以现在我们要给NestedRecord<K, V>下定义.


这里有一种方法:

type NestedRecord<K extends string, V> = [K] extends [never] ? unknown : {
  [P in (K extends `${infer K0}.${string}` ? K0 : K)]:
  (P extends K ? V : unknown) &
  NestedRecord<K extends `${P}.${infer KR}` ? KR : never, V>
}

这是使用key remapping in mapped typestemplate literal types来解析K中的密钥.NestedRecord<K, V>的关键字应该只是K的第一个点之前的部分,或者K的任何没有点的部分.对于没有点的任何K,值NestedRecord<K, V>应该是V,对于从当前关键点开始的第一个点之后的K的部分,值应该是intersectedNestedRecord<K, V>.

哦,NestedRecord<never, V>应该是unknown而不是{},这样我们就不会有很多令人讨厌的交叉点了,{}在末尾类型.

您可以验证对于上面的Example,此定义的行为是否符合预期.


最后,我们需要实施exists()个.或许有一种方法可以做到这一点:

function exists<T extends object, K extends string>(
  obj: T, paths: K[]
): obj is T & NestedRecord<K, {}> {
  return paths.every(p => p.split(".").reduce((a, k) => (a ?? a[k]), obj) != null);
}

我使用every()reduce()数组方法来验证objectpaths中的每条路径上都有一个非空值.


让我们在您的示例object上try 一下:

object;
// ^? var object: Foo
if (exists(object, ["a.ii", "c"])) {
  object;
  // ^? var object: Foo & { a: { ii: {}; }; c: {}; }
  let value = object.a.ii;
  // ^? let value: number
  console.log(value); // 1
}

看上go 不错.在受保护的代码块中,object已经从Foo缩小到Foo & { a: { ii: {}; }; c: {}; },这意味着当我们深入object.a.ii时,类型是(number | undefined) & {},这只是number,这是所需的.

Playground link to code

Typescript相关问答推荐

当两个参数具有相同的通用类型时,如果没有第一个参数,则递减约束默认会导致第二个参数的错误推断

根据另一个成员推断类成员的类型

typescript抱怨值可以是未定义的,尽管类型没有定义

TypeScript类型不匹配:在返回类型中处理已经声明的Promise Wrapper

当使用`type B = A`时,B的类型显示为A.为什么当`type B = A时显示为`any `|用A代替?

对深度对象键/路径进行适当的智能感知和类型判断,作为函数参数,而不会触发递归类型限制器

键集分页顺序/时间戳上的筛选器,精度不相同

如何使用WebStorm调试NextJS的TypeScrip服务器端代码?

如何调整对象 struct 复杂的脚本函数的泛型类型?

诡异的文字类型--S为物语的关键

在Reaction-Native-ReAnimated中不存在Animated.Value

记录键的泛型类型

如何从MAT-FORM-FIELD-MAT-SELECT中移除焦点?

如何避免从引用<;字符串&>到引用<;字符串|未定义&>;的不安全赋值?

如果判断空值,则基本类型Gaurd不起作用

使用来自API调用的JSON数据的Angular

接口中可选嵌套属性的类型判断

如何创建一个将嵌套类属性的点表示法生成为字符串文字的类型?

是否使用类型将方法动态添加到类?

Svelte+EsBuild+Deno-未捕获类型错误:无法读取未定义的属性(读取';$$';)