【问题标题】:How to use a literal string argument in a Typescript generic constraint for deep object path?如何在 Typescript 通用约束中为深层对象路径使用文字字符串参数?
【发布时间】:2021-03-21 12:54:27
【问题描述】:

我正在尝试编写一个对象路径实用程序,以确保键列表对应于指定泛型类型中的实际路径。

在我下面的尝试中,我认为通用签名将推断的文字字符串值扩大到简单地为string。如何在我的通用约束中使用文字字符串参数来确保下一个键是前一个键值的键?

class FieldPathBuilder<C,
  K1 extends string & keyof C = string & keyof C, // ensure first arg is a valid top-level key
  K2 extends string & keyof C[K1] = string & keyof C[K1], // must be a valid key of C[K1]
> {

  path(key1: K1, key2?: K2): string {
    return '' // not important
  }
}

interface A {
  first: {
    second: {
      third: {}
    }
  },
  other: {} // NOTE: the example below compiles if I remove this field
}

new FieldPathBuilder<A>().path('first', 'second') // <-- Error: Argument of type
// '"second"' is not assignable to parameter of type 'undefined'

【问题讨论】:

    标签: typescript generics typescript-generics


    【解决方案1】:

    问题似乎是通用键约束是在类级别而不是在函数中声明的,我假设这会阻止编译器将字符串类型推断为文字值,因为直到跟随函数调用。

    ✅ 将键类型约束从类移动到函数

    interface A {
      first: { second: { third: {} } },
      other: {}
    }
    
    class FieldPathBuilder<C> {
    
      path<K1 extends string & keyof C, K2 extends string & keyof C[K1]>(
        key1: K1,
        key2?: K2,
      ): string {
        return '' // not important
      }
    }
    
    new FieldPathBuilder<A>().path('first', 'second') // ✅
    new FieldPathBuilder<A>().path('other', 'second') // ❌ "second" isn't key of "other"
    

    ❌ 在类级别保留键类型约束并在构造函数中接受键

    将键传递给构造函数可能更简洁,但这需要将键作为泛型类型参数和函数参数传递,因为如果提供了所有非默认泛型类型,则必须提供所有非默认泛型类型。在这种情况下,A 被给出,所以我们还需要提供显式的键类型。

    虽然这可行,但它不是我正在寻找的解决方案,因为您需要指定每个键两次。

    interface A {
      first: { second: { third: {} } },
      other: {}
    }
    
    class FieldPathBuilder<
      C,
      K1 extends string & keyof C,
      K2 extends string & keyof C[K1]
    > {
      constructor(key1: K1, key2?: K2) {}
    }
    
    new FieldPathBuilder<A>('first', 'second') // ❌ Expected 3 type arguments, but got one
    new FieldPathBuilder<A, 'first', 'second'>('first', 'second') // ✅
    

    ❌ 使用默认键类型

    Typescript 支持默认类型参数,因此我们可以尝试通过设置默认值来避免多余的字符串类型参数。

    class FieldPathBuilder<C,
      K1 extends string & keyof C = string & keyof C, // ? default
      K2 extends string & keyof C[K1] = string & keyof C[K1] // ? default
    > {
      constructor(key1: K1, key2?: K2) {}
    }
    
    new FieldPathBuilder<A>('first', 'second') // ❌
    

    但是,这仍然无法编译,因为(我认为)默认键类型现在已扩展到非文字键字符串值(string 而不是 "first"...)。

    【讨论】:

      【解决方案2】:

      我可以建议这种方法(安全路径构建器):

      interface PathResult<T> {
        isSafe: boolean;
        path: any[];
        <TKey extends keyof T>(subkey: TKey): PathResult<T[TKey]>;
      }
      
      /**
       * Create a deep path builder for a given type
       */
      export function safePath<T>(root: T) {
        /**Returns a function that gets the next path builder */
        function subpath<T, TKey extends keyof T>(parent: string[], subRoot: T, key: TKey): PathResult<T[TKey]> {
          const newPath = [...parent, key as string];
          let newRoot: T[TKey];
          let isSafe: boolean;
          if (isSafe = (subRoot && subRoot[key] !== undefined)) {
            newRoot = subRoot[key];
          } else {
            isSafe = false;
          }
          const x = (<TSubKey extends keyof T[TKey]>(subkey: TSubKey) => subpath<T[TKey], TSubKey>(newPath, newRoot, subkey)) as PathResult<T[TKey]>;
          x.path = newPath;
          x.isSafe = isSafe;
          return x;
        }
      
        return <TKey extends keyof T>(key: TKey) => subpath<T, TKey>([], root, key);
      }
      
      interface MyInterface {
        person: {
          names?: {
            lastnames?: {
              first: string;
              second: string;
            }
            firstname: string;
          }
          age: number;
        }
        other: string;
      }
      
      // example 2
      const test: MyInterface = {
        person: {
          age: 123
        },
        other: 'foo'
      };
      const res2 = safePath(test)('person')('names')('lastnames')('second');
      console.log(res2.path); // ["person", "names", "lastnames", "second"]
      console.log(res2.isSafe); // false
      
      // example 3
      const res3 = safePath(test)('person')('foo')('lastnames')('second'); // compile error
      console.log(res3.path); // ["person", "foo", "lastnames", "second"]
      console.log(res3.isSafe); // false
      
      // example 4, type `(` the ctrl + space see suggestions for "person" and "other"
      const res4 = safePath(test);
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-05-21
        • 2022-01-09
        • 1970-01-01
        • 2017-05-19
        • 1970-01-01
        • 2012-02-07
        • 2017-11-25
        相关资源
        最近更新 更多