如何在 TypeScript 中深入对象并断言它们的类型?

huangapple go评论71阅读模式
英文:

How to drill into objects and assert their type with TypeScript?

问题

Certainly, you can simplify the assertions and conditional checks in your TypeScript code by creating utility functions to handle these tasks. Here's an example of how you can achieve a more concise and readable code structure:

type ContainerType = {
  effect: EffecType | string | number | null | boolean
}

type EffecType = {
  [key: string]: ContainerType
}

type InputParamsType = {
  id: string
  language_id: string
  verified?: boolean
}

function assertUUID(obj: any, path: string) {
  const value = getObjectValue(obj, path);
  if (typeof value !== 'string') {
    throw new Error('not_string');
  }
}

function isBoolean(obj: any, path: string): boolean {
  const value = getObjectValue(obj, path);
  return typeof value === 'boolean';
}

function getObjectValue(obj: any, path: string) {
  const keys = path.split('.');
  let value = obj;

  for (const key of keys) {
    if (value && typeof value === 'object' && key in value) {
      value = value[key];
    } else {
      return undefined; // Property not found, return undefined
    }
  }

  return value;
}

export async function insert(effect: EffecType) {
  const id = '123';

  assertUUID(effect, 'language.effect.id.effect');

  const input: InputParamsType = {
    id,
    language_id: effect.language.effect.id.effect,
  };

  if (isBoolean(effect, 'verified.effect')) {
    input.verified = effect.verified.effect;
  }

  console.log(input); // do some db saving here...
}

In this updated code, we've created utility functions assertUUID, isBoolean, and getObjectValue to handle the assertions and value retrieval based on the provided paths. This approach reduces the need for excessive nesting and repetitive code for each assertion or conditional check.

英文:

I am doing this a lot in my TypeScript project (here is a playground):

type ContainerType = {
effect: EffecType | string | number | null | boolean
}
type EffecType = {
[key: string]: ContainerType
}
function isObject(x: unknown): x is object {
return typeof x === 'object'
}
function assertObject(x: unknown): asserts x is object {
if (!isObject(x)) {
throw new Error('not_object')
}
}
function assertUUID(x: unknown): asserts x is string {
if (typeof x !== 'string') {
throw new Error('not_string')
}
}
function isContainer(x: unknown): x is ContainerType {
return !Array.isArray(x) && isObject(x)
}
function assertContainer(x: unknown): asserts x is ContainerType {
if (!isContainer(x)) {
throw new Error('not_effect')
}
}
function isBoolean<T>(x: unknown): x is boolean {
return typeof x === 'boolean'
}
type InputParamsType = {
id: string
language_id: string
verified?: boolean
}
export async function insert(effect: EffecType) {
const id = '123'
assertObject(effect)
assertContainer(effect.language)
assertObject(effect.language.effect)
assertContainer(effect.language.effect.id)
assertUUID(effect.language.effect.id.effect)
const input: InputParamsType = {
id,
language_id: effect.language.effect.id.effect,
}
if (isContainer(effect.verified)) {
if (isBoolean(effect.verified.effect)) {
input.verified = effect.verified.effect
}
}
console.log(input) // do some db saving here...
}

Notice how I am doing 2 annoying things:

  1. assertions slowly down the branch of the tree to get to language id (effect.x.effect.x...). At least it's flat and not nested.
  2. then conditional checks slowly down the tree in a nested fashion to get to the verified prop.
  3. I have 20 other properties I need to do this for, just for this model.

How can I do something more like this with TypeScript?

assertUUID(effect, 'language.effect.id.effect')
const input: InputParamsType = {
id,
language_id: effect.language.effect.id.effect,
};
if (isBoolean(effect, 'verified.effect')) {
input.verified = effect.verified.effect;
}

In that way, there is only minimal assertions and nesting required to make it work. Is it possible somehow? Doesn't have to be that exact API if there is an alternative way to somehow get rid of all these initial declarations and nestings from the first snippet.

答案1

得分: 2

这个方法使用了点路径字符串,正如问题中建议的。首先,让我们创建一个NestedRecord<K, V>实用类型,该类型接受一个点路径字符串K和一个值V,并生成具有适当形状的对象类型:

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

这是一个递归定义的条件类型,它将K解析为第一个点之前的部分K0和点之后的部分KR,并生成具有键K0和值NestedRecord<KR, V>的对象。如果K没有点,那么我们只需要等同于Record<K, V>的等效部分。

现在让我们创建一个Merge<T, U>实用类型,它深度合并类型T和类型U,假设它们之间没有冲突。在理想情况下,这可以简单地使用交集T & U来实现,但这似乎与索引签名不太兼容。以下是一种似乎有效的方法:

type Merge<T, U> = Extract<
  T extends object ? U extends object ? { [K in keyof (T & U)]:
    K extends keyof T ? K extends keyof U ?
    Merge<T[K], U[K]> : T[K] :
    K extends keyof U ? U[K] : never } : U : T & U,
  T
>;

基本上,我们在TU中递归地深入合并它们,直到遇到非对象,然后返回非对象(以便string & {a:{b:{c:⋯}}}仅为string以保持简单性)或两者的交集。并且使用Extract实用类型来说服编译器Merge<T, U>始终可以分配给T


拥有了这些工具,我们可以创建我们的函数。让我们编写一个通用的 自定义类型守卫函数,它验证输入x的属性在嵌套路径path上是否由输入类型守卫函数guard守卫:

function guardPath<T, K extends string, V>(
  x: T, path: K, guard: (x: any) => x is V
): x is Merge<T, NestedRecord<K, V>> {
  for (const k of path.split(".")) {
    if (x == null) return false; // 无法对其进行索引
    x = (x as any)[k];
  }
  return guard(x);
}

这表示输入的类型是T,如果为true,输出类型是Merge<T, NestedRecord<K, V>>,其中Kpath的类型,V是由guard守卫的类型。实现只是将path在点处拆分,并迭代下标,直到要么遇到无法索引的东西,要么可以将守卫应用于最终值。

你还想要一个断言函数,以便我们可以使用guardPath来实现它... 这非常相似:

function assertPath<T, K extends string, V>(
  x: T, path: K, guard: (x: any) => x is V, vname: string = "valid value"
): asserts x is Merge<T, NestedRecord<K, V>> {
  if (!guardPath(x, path, guard))
    throw new Error("no " + vname + " found at path " + path)
};

唯一的区别是请求V类型的名称,以便错误消息有意义。


好的,让我们试试吧!这里有一些基本的类型守卫函数:

const isString = (x: any): x is string => typeof x === "string";
const isBoolean = (x: any): x is boolean => typeof x === "boolean";
const isNumber = (x: any): x is number => typeof x === "number";

鉴于你的ContainerTypeEffecType(拼写有误)和InputParamsType,以下是insert()的可能实现:

function insert(effect: EffecType) {
  const id = '123'

  assertPath(effect, "language.effect.id.effect", isString, "string");
  assertPath(effect, "language.effect.otherProp.effect", isNumber, "number");
  const input: InputParamsType = {
    id,
    language_id: effect.language.effect.id.effect,
  }
  if (guardPath(effect, "verified.effect", isBoolean)) {
    input.verified = effect.verified.effect
  }
  console.log(input) // 这里进行一些数据库保存...
}

在函数内部,在写input.verified =的位置,你可以检查effect的类型,看起来是这样的:

/* (parameter) effect: {
  [x: string]: ContainerType;
  language: {
      effect: {
        [x: string]: ContainerType;
        id: {
            effect: string;
        };
        otherProp: {
            effect: number;
        };
    };
};
verified: {
    effect: boolean;
}; } */

这是三个Merge的结果。当调用它时,它会按预期工作:

insert({
  language: {
    effect: {
      id: { effect: "abcde" },
      otherProp: { effect: 123 }
    }
  },
  verified: { effect: true }
});
/* {
  "id": "123

<details>
<summary>英文:</summary>

This approach uses dotted path strings, as suggested in the question.  First let&#39;s make a `NestedRecord&lt;K, V&gt;` utility type which takes a dotted path string `K` and a value `V` and produces an object type with the appropriate shape:

    type NestedRecord&lt;K extends string, V&gt; =
      K extends `${infer K0}.${infer KR}` ?
      { [P in K0]: NestedRecord&lt;KR, V&gt; } :
      { [P in K]: V };
    
That&#39;s a recursively-defined [conditional type](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) that parses `K` into the part `K0` before the first dot and the part `KR` after it, and produces an object with key `K0` and value `NestedRecord&lt;KR, V&gt;`. If `K` has no dot then we&#39;re at the last path segment and we just need the equivalent of `Record&lt;K, V&gt;`.

Now let&#39;s make a `Merge&lt;T, U&gt;` utility type that deeply merges a type `T` with a type `U`, assuming that there&#39;s no conflict between them.  In a perfect world this could just be the [intersection](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types) `T &amp; U`, but that doesn&#39;t seem to play nicely with [index signatures](https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures).  This seems to work:

    type Merge&lt;T, U&gt; = Extract&lt;
      T extends object ? U extends object ? { [K in keyof (T &amp; U)]:
        K extends keyof T ? K extends keyof U ?
        Merge&lt;T[K], U[K]&gt; : T[K] :
        K extends keyof U ? U[K] : never } : U : T &amp; U,
      T
    &gt;

Essentially we are walking down through `T` and `U` and recursively merging them until we hit non-objects, and then returning either just the non-object (so that `string &amp; {a:{b:{c:⋯}}}` is just `string` for simplicity) or the intersection for both non-objects.  And one big use of [the `Extract` utility type](https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union) to convince the compiler that `Merge&lt;T, U&gt;` is always assignable to `T`.

---

Armed with those we can make our functions.  Let&#39;s write a [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) [custom type guard function](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) that verifies that the property of the input `x` at nested path `path` has the type guarded by an input type guard function `guard`:


    function guardPath&lt;T, K extends string, V&gt;(
      x: T, path: K, guard: (x: any) =&gt; x is V
    ): x is Merge&lt;T, NestedRecord&lt;K, V&gt;&gt; {
      for (const k of path.split(&quot;.&quot;)) {
        if (x == null) return false; // can&#39;t index into this
        x = (x as any)[k];
      }
      return guard(x);
    }


This says that the input is of type `T` and the output, if true, is `Merge&lt;T, NestedRecord&lt;K, V&gt;&gt;` where `K` is the type of `path` and `V` is the type guarded by `guard`.  The implementation just splits `path` at dots and iterates indexing down until it either hits something it can&#39;t index into or can apply the guard to the final value.

You also wanted an [assertion function](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions) so we can use `guardPath` to implement that... it&#39;s quite similar:

    function assertPath&lt;T, K extends string, V&gt;(
      x: T, path: K, guard: (x: any) =&gt; x is V, vname: string = &quot;valid value&quot;
    ): asserts x is Merge&lt;T, NestedRecord&lt;K, V&gt;&gt; {
      if (!guardPath(x, path, guard))
        throw new Error(&quot;no &quot; + vname + &quot; found at path &quot; + path)
    };

The only difference is asking for a name for the type of `V` so that the error message is meaningful.

----

Okay, let&#39;s try it out!  Here are some basic type guard functions:

    const isString = (x: any): x is string =&gt; typeof x === &quot;string&quot;;
    const isBoolean = (x: any): x is boolean =&gt; typeof x === &quot;boolean&quot;;
    const isNumber = (x: any): x is number =&gt; typeof x === &quot;number&quot;;

And given your `ContainerType`, `EffecType` (*sic*), and `InputParamsType`, here&#39;s a possible implementation of `insert()`:   

    function insert(effect: EffecType) {
      const id = &#39;123&#39;
    
      assertPath(effect, &quot;language.effect.id.effect&quot;, isString, &quot;string&quot;);
      assertPath(effect, &quot;language.effect.otherProp.effect&quot;, isNumber, &quot;number&quot;);
      const input: InputParamsType = {
        id,
        language_id: effect.language.effect.id.effect,
      }
      if (guardPath(effect, &quot;verified.effect&quot;, isBoolean)) {
        input.verified = effect.verified.effect
      }
      console.log(input) // do some db saving here...
    }

Inside the function at the point we write `input.verified =`, you can inspect `effect` to see its type is

    /* (parameter) effect: {
      [x: string]: ContainerType;
      language: {
          effect: {
            [x: string]: ContainerType;
            id: {
                effect: string;
            };
            otherProp: {
                effect: number;
            };
        };
    };
    verified: {
        effect: boolean;
    }; } */

which is the result of three `Merge`s.  And when you call it, it behaves as expected:

    insert({
      language: {
        effect: {
          id: { effect: &quot;abcde&quot; },
          otherProp: { effect: 123 }
        }
      },
      verified: { effect: true }
    });
    /* {
      &quot;id&quot;: &quot;123&quot;,
      &quot;language_id&quot;: &quot;abcde&quot;,
      &quot;verified&quot;: true
    }  */
    
    try {
      insert({
        language: { effect: &quot;oopsie&quot; }
      });
      // &#128165; RUNTIME ERROR! no string found at path language.effect.id.effect 
    } catch (e) {
      console.log(e);
    }


[Playground link to code](https://www.typescriptlang.org/play?#code/FAFwngDgpgBAclAziKATASlAxgewE6oA8A0jFAB4oB2qiMyeAllQOYA0MAagHwwC8wGDFIVqtGAAMAJAG9mAMyh5hABgC+AOlkKlw9GokwA-IJgyYAbQAKMZqoC6ALnhIUGbPiLF0HHjDUwjqbm1rZUwk5c-gDcwKCQsACySixQhAAqHACqvHwwAKKUeACGWCCEpulklFA0dDgARgBW2CDGMFnVYvXNre0hpHYA1lBgOPIwABRVAGQdAJROpkIiNXUwI2MTVUbCXbXim+MdxsswyXipGRbE9tk39rzO6Q+BZ6vdG6PHnbtZr84qFAAG66ALOTrPGBzLJsSrAbhxYDyACuVDKjBw4RYKOKBCsxRAAAsMhwPgc6AxmOwuNxJqZyM8OBBCUTnMQODi8ahnJNGTBilQwPN+LxyLY6JxgPNnOLGHQLldMi5kGhMLgCCRfNxeDJTPJ8FNcFRkBsYMcWcSNIgIAAbRggSYAIg0TvmIr1QiEjAmfP4eSoKNttpFeCgIBReHC8mKtsQUGiMAA9EmYFhBQByNrMVAUMIgHAwYnys7ivJ+4p0QXCixDeyxIRqUxhiNRmBcgh8+axJuo9EgTHhSvxvAgAnE0l7UQU+ggJisbX0oT85WWtnCTm4gi8-nVkV8MUSrgcYFUYoAWygziprH4MCdwNjjFQMEftpRUCd0ucw6UIDocoKikaTKggqruBqXjarqpg+lMACEHaoOORJ8syrKbty7pnMSeA4AA7jAQKEfkeB4XgzpUIWTowAA1K+Z6XnR94wAaaIvoSMBrix9FrvMwBqLEwDGqa8oAMpztSd58j+QoyjAgGzvOLCikWCTHGWfB5E6N4sE6sQidmiAAEI4DgtpQIK0m7nJspHg0ZkWVZB5qdAGn+tpDnmZZVD6cJWKiYgcAoueDS6OWNnCnZ8pESFYXKC54BuRMmnaYGoVKH58TQDAADCWIgMUzBKOkCR3uYUDyIoZTOPkVXYKVOUAD5KVJLXpfFMDtUGtpdTAXlOeETZJbAdXVY1sB5CEmzXpJrCRPlVCFcVeATf4SIjTAACSVAQCiY54heiBrVNsE8q1rCmLagpcqkAD6z6zcppigkw8iMGgRjOANPkCXEfYYliYQjo6lXVSAtX1VgE0eqYhm2C+eQZgAjAATAAzBmcRCL+o4oZMYOtBwTrXawuKpBohNlBoz6U1DIBOhw4lzTSOks26DYCogIP41TIDE6Tt1QHT4MaDgxJKFYeEQCLrSMxKwUZXgxMdZl3ZwwF2a7ftzg7XtB0lOex1ladXoI3CZuC+TUAPedfMaFbxQU-btN8xb63er6SG8-TxOvT6H2oLLZTy-KpneYK7pmGcfPJgAVFMLKG+GSginzziel6Fj8rpC0FUVQKrQknNCI7qQZ2cZvp9HZu11nOcs3nS0FyVxeV3XCMVx33dkPTT3UiXPeCe3dfi0SkvS13Pd19Xqt4IP3fD4vg9L16-vvWgU8z33-WOT5K89jAcdJmczD6xo6+B3e9uX2gwcgKYTZIkIIneQ7OAsJMZ-7SKKYwKghZEA4CYqgBo9BijAikuPMMGhYF-WRGiQG0YzIEx3mNBqCRYY425n+H24MOAZhRCOe+NMg58wzEzRAEllL8WwTzVkqD8EwAzI0Fo1MXZkPphQiU1DqS0K5vQ4kjCibMNYa0EhI177cOZjQ0w9sxHsPphoSR9sCxZAgNAPAuVKxQEmOrJ+cRmAg0mJnMuV4a5emrpnM2j0zC93Bs4J0xQGhYFzDRNQ7szZjwnjgCAGd7GtGcGjdGHsvRNkbO7W+50Ko7znB+daah1ZJgTpnJ0z4nSOOCYzUwJMbrW1thk+8zjXGfndg+JQAc0CFLiVAASQhj5xDnGACxRi-wmLOGY-x1cnRmQgIgD67jH7qyEH-QAvBuAFKdmA6AshwHSFtRI+QCjoHQAAeXQPBIigCWasRwOxAUbRuJmJIa7emMA6nphAFgIkUwoBYLTAFN+toP4E30UAA)


</details>



# 答案2
**得分**: 1

Here is the translated content:

一个方法是将具体示例对象传递给一个函数,使TypeScript可以处理更简单的类型(避免递归等路径导向方法所需的操作)。不过这会导致代码有点冗长。

这里有一个示例,不试图处理联合类型:

```lang-typescript
const effect: any = {/*...*/};

effect;
// ^? −−− const effect: any

assertMatch(effect, { object: { effect: { id: { effect: &quot;&quot; } } } }, &quot;invalid_object_id&quot;);
assertMatch(effect, { object: { effect: { type: { effect: &quot;&quot; } } } }, &quot;invalid_object_type&quot;);
// 或者使用这种方式:
// assertMatch(effect, { object: { effect: { id: { effect: &quot;&quot; }, type: { effect: &quot;&quot; } } } }, &quot;invalid_object_id_or_type&quot;);

effect;
// ^? −−− const effect: { object: { effect: { id: { effect: string } } } } &amp; { object: { effect: { type: { effect: string } } } };

const id = &quot;123&quot;;

const input = {
    id,
    object_id: effect.object.effect.id.effect,
    object_type: effect.object.effect.type.effect,
};

// 稍微改进的 `typeof`
function getType(x: any) {
    const rawType = typeof x;
    if (rawType === &quot;object&quot; &amp;&amp; !x) {
        return &quot;null&quot;;
    }
    return rawType;
}

export function assertMatch&lt;TargetType&gt;(
    x: any,
    example: TargetType,
    message: string = &quot;invalid value&quot;
): asserts x is TargetType {
    // &gt;&gt;&gt; 不完全和即兴而发,不打算成为全能的实现,只是个草图 &lt;&lt;&lt;
    const typex = getType(x);
    if (typex !== getType(example)) {
        throw new Error(message);
    }
    if (typex === &quot;object&quot;) {
        for (const name in example) {
            assertMatch(x[name], example[name], message);
        }
    }
}

Playground链接

这涉及创建和处理示例对象,但JavaScript/TypeScript代码中经常创建和处理短暂的小对象,JavaScript引擎非常擅长此操作。

如果需要处理联合类型,可以通过传递多个示例对象来扩展这个方法。大致如下所示:

const effect: any = {/*...*/};
effect;
// ^?
assertMatchesAny(
effect,
[
{ object: { effect: { id: { effect: 0 } } } },  // 一个数字...
{ object: { effect: { id: { effect: &quot;&quot; } } } }, // ...或一个字符串
],
&quot;invalid_object_id&quot;
);
assertMatchesAny(effect, [{ object: { effect: { type: { effect: &quot;&quot; } } } }], &quot;invalid_object_type&quot;);
// 或者使用这种方式:
// assertMatch(effect, { object: { effect: { id: { effect: &quot;&quot; }, type: { effect: &quot;&quot; } } } }, &quot;invalid_object_id_or_type&quot;);
effect;
// ^?
const id = &quot;123&quot;;
const input = {
id,
object_id: effect.object.effect.id.effect,
object_type: effect.object.effect.type.effect,
};
// 稍微改进的 `typeof`
function getType(x: any) {
const rawType = typeof x;
if (rawType === &quot;object&quot; &amp;&amp; !x) {
return &quot;null&quot;;
}
return rawType;
}
export function matchesAny&lt;TargetType&gt;(
x: any,
examples: TargetType[],
): x is TargetType {
for (const example of examples) {
// &gt;&gt;&gt; 不完全和即兴而发,不打算成为全能的实现,只是个草图 &lt;&lt;&lt;
let matches = true;
const typex = getType(x);
if (typex !== getType(example)) {
matches = false;
} else if (typex === &quot;object&quot;) {
for (const name in example) {
matches = matchesAny(x[name], [example[name]]);
if (!matches) {
break;
}
}
}
if (matches) {
return true;
}
}
return false;
}
export function assertMatchesAny&lt;TargetType&gt;(
x: any,
examples: TargetType[],
message: string = &quot;invalid value&quot;
): asserts x is TargetType {
if (!matchesAny(x, examples)) {
throw new Error(message);
}
}

Playground链接

英文:

One approach would be to have a function you passed a concrete example object into, allowing TypeScript to work with simpler types (avoiding recursion and such that a path-based approach would need). It does have the issue of being a bit verbose, though.

Here's an example that doesn't attempt to handle union types:

const effect: any = {/*...whatever...*/};
effect;
// ^? −−− const effect: any
assertMatch(effect, { object: { effect: { id: { effect: &quot;&quot; } } } }, &quot;invalid_object_id&quot;);
assertMatch(effect, { object: { effect: { type: { effect: &quot;&quot; } } } }, &quot;invalid_object_type&quot;);
// Or alternatively:
// assertMatch(effect, { object: { effect: { id: { effect: &quot;&quot; }, type: { effect: &quot;&quot; } } } }, &quot;invalid_object_id_or_type&quot;);
effect;
// ^? −−− const effect: { object: { effect: { id: { effect: string } } } } &amp; { object: { effect: { type: { effect: string } } } };
const id = &quot;123&quot;;
const input = {
id,
object_id: effect.object.effect.id.effect,
object_type: effect.object.effect.type.effect,
};
// Marginally improved `typeof`
function getType(x: any) {
const rawType = typeof x;
if (rawType === &quot;object&quot; &amp;&amp; !x) {
return &quot;null&quot;;
}
return rawType;
}
export function assertMatch&lt;TargetType&gt;(
x: any,
example: TargetType,
message: string = &quot;invalid value&quot;
): asserts x is TargetType {
// &gt;&gt;&gt; Incomplete and off-the-cuff, not meant to be an all-singing, all-dancing
// implementation, just a sketch &lt;&lt;&lt;
const typex = getType(x);
if (typex !== getType(example)) {
throw new Error(message);
}
if (typex === &quot;object&quot;) {
for (const name in example) {
assertMatch(x[name], example[name], message);
}
}
}

Playground link

That involves creating and disposing the example objects, but short-lived small objects are constantly created and disposed in JavaScript/TypeScript code and JavaScript engines are very good at it.

That can probably be expanded to handle unions, if you need to handle them, by passing on multiple example objects. Something along these lines:

const effect: any = {/*...*/};
effect;
// ^?
assertMatchesAny(
effect,
[
{ object: { effect: { id: { effect: 0 } } } },  // a number...
{ object: { effect: { id: { effect: &quot;&quot; } } } }, // ...or a string
],
&quot;invalid_object_id&quot;
);
assertMatchesAny(effect, [{ object: { effect: { type: { effect: &quot;&quot; } } } }], &quot;invalid_object_type&quot;);
// Or alternatively:
// assertMatch(effect, { object: { effect: { id: { effect: &quot;&quot; }, type: { effect: &quot;&quot; } } } }, &quot;invalid_object_id_or_type&quot;);
effect;
// ^?
const id = &quot;123&quot;;
const input = {
id,
object_id: effect.object.effect.id.effect,
object_type: effect.object.effect.type.effect,
};
// Marginally improved `typeof`
function getType(x: any) {
const rawType = typeof x;
if (rawType === &quot;object&quot; &amp;&amp; !x) {
return &quot;null&quot;;
}
return rawType;
}
export function matchesAny&lt;TargetType&gt;(
x: any,
examples: TargetType[],
): x is TargetType {
for (const example of examples) {
// &gt;&gt;&gt; Incomplete and off-the-cuff, not meant to be an all-singing, all-dancing
// implementation, just a sketch &lt;&lt;&lt;
let matches = true;
const typex = getType(x);
if (typex !== getType(example)) {
matches = false;
} else if (typex === &quot;object&quot;) {
for (const name in example) {
matches = matchesAny(x[name], [example[name]]);
if (!matches) {
break;
}
}
}
if (matches) {
return true;
}
}
return false;
}
export function assertMatchesAny&lt;TargetType&gt;(
x: any,
examples: TargetType[],
message: string = &quot;invalid value&quot;
): asserts x is TargetType {
if (!matchesAny(x, examples)) {
throw new Error(message);
}
}

Playground link

huangapple
  • 本文由 发表于 2023年5月14日 09:25:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/76245460.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定