代码之家  ›  专栏  ›  技术社区  ›  Nathan Bierema

在TypeScript中映射泛型扩展参数类型

  •  8
  • Nathan Bierema  · 技术社区  · 6 年前

    我正在尝试编写一个泛型方法,它接受作为对象键的任意数量的参数,并将它们的键的值用作构造函数的参数。这是我最初的实现:

    // Typescript 2.x
    export function oldMethod<TProps>() {
      function create<
        TInstance extends Geometry | BufferGeometry,
      >(
        geometryClass: new () => TInstance,
      ): any;
      function create<
        TInstance extends Geometry | BufferGeometry,
        TKey1 extends Extract<keyof TProps, string>,
      >(
        geometryClass: new (param1: TProps[TKey1]) => TInstance,
        key1: TKey1,
      ): any;
      function create<
        TInstance extends Geometry | BufferGeometry,
        TKey1 extends Extract<keyof TProps, string>,
        TKey2 extends Extract<keyof TProps, string>,
      >(
        geometryClass: new (param1: TProps[TKey1], param2: TProps[TKey2]) => TInstance,
        key1: TKey1,
        key2: TKey2,
      ): any;
      function create<
        TInstance extends Geometry | BufferGeometry,
        TKey1 extends Extract<keyof TProps, string>,
        TKey2 extends Extract<keyof TProps, string>,
        TKey3 extends Extract<keyof TProps, string>,
      >(
        geometryClass: new (param1: TProps[TKey1], param2: TProps[TKey2], param3: TProps[TKey3]) => TInstance,
        key1: TKey1,
        key2: TKey2,
        key3: TKey3,
      ): any;
      // ...all the way up to 8 possible keys
      function create<TInstance extends Geometry | BufferGeometry>(
        geometryClass: new (...args: Array<TProps[Extract<keyof TProps, string>]>) => TInstance,
        ...args: Array<Extract<keyof TProps, string>>) {
        class GeneratedGeometryWrapper extends GeometryWrapperBase<TProps, TInstance> {
          protected constructGeometry(props: TProps): TInstance {
            return new geometryClass(...args.map((arg) => props[arg]));
          }
        }
    
        return class GeneratedGeometryDescriptor extends WrappedEntityDescriptor<GeneratedGeometryWrapper,
          TProps,
          TInstance,
          GeometryContainerType> {
          constructor() {
            super(GeneratedGeometryWrapper, geometryClass);
    
            this.hasRemountProps(...args);
          }
        };
      }
      return create;
    }
    

    随着宣布在TypeScript3.0中提取和扩展带元组的参数列表,我希望能够删除重载,使其更简单:

    // Typescript 3.x
    export function newMethod<TProps>() {
      function create<TInstance extends Geometry | BufferGeometry, TArgs extends Array<Extract<keyof TProps, string>>>(
        geometryClass: new (...args: /* what goes here? */) => TInstance,
        ...args: Array<Extract<keyof TProps, string>>) {
        class GeneratedGeometryWrapper extends GeometryWrapperBase<TProps, TInstance> {
          protected constructGeometry(props: TProps): TInstance {
            return new geometryClass(...args.map((arg) => props[arg]));
          }
        }
    
        return class GeneratedGeometryDescriptor extends WrappedEntityDescriptor<GeneratedGeometryWrapper,
          TProps,
          TInstance,
          GeometryContainerType> {
          constructor() {
            super(GeneratedGeometryWrapper, geometryClass);
    
            this.hasRemountProps(...args);
          }
        };
      }
      return create;
    }
    

    但是,我不知道该把什么作为 args 定义构造函数类型的。如果我能够像在JavaScript中操作对象那样操作类型,我会这样编写它: ...[...TArgs].map(TArg => TProps[TArg] ,但显然这不是有效的TypeScript语法,我想不出任何方法来实现它。我错过了表达这种类型的方法吗?有没有什么方法可以使这个类型完全安全,而不必有函数重载和有限数量的参数?是否缺少某种类型脚本功能,使我可以表达这种类型?

    1 回复  |  直到 6 年前
        1
  •  7
  •   jcalz    6 年前

    我在下面的例子中去掉了很多代码,但是应该是同样的精神。

    您缺少的功能称为 mapped arrays/tuples 计划于年发布 TypeScript 3.1 2018年8月的某个时候。您将能够像映射其他类型一样映射数组和元组,如下所示:

    type Mapped<T> = {[K in keyof T]: Array<T[K]>};
    type Example = Mapped<[string, number, boolean]>; 
    // type Example = [string[], number[], boolean[]];
    

    如果你用 typescript@next 现在你可以试试。


    在你的情况下 希望 做的事情就像

    type MappedArgs = {[K in keyof TArgs]: TProps[TArgs[K]]};
    type ConstructorType = new (...args: MappedArgs) => any;
    

    但有几个突出的问题阻止你这么做。一是由于某种原因编译器还不明白 TArgs[K] 是的有效索引 TProps . 所以你可以介绍 conditional type 这样你就可以解决这个问题:

    type Prop<T, K> = K extends keyof T ? T[K] : never;
    type MappedArgs = {[K in keyof TArgs]: Prop<TProps,TArgs[K]>};
    

    但以下方法仍然不起作用:

    type ConstructorType = new (...args: MappedArgs) => any;
    // error, a rest parameter must be of an array type
    

    隐马尔可夫模型, MappedArgs 当然是数组类型,但TypeScript没有意识到这一点。这也不能让人信服:

    type MappedArgs = {[K in keyof TArgs]: Prop<TProps,TArgs[K]>} 
      & unknown[]; // definitely an array!
    
    type ConstructorType = new (...args: MappedArgs) => any;
    // error, a rest parameter must be of an array type 
    

    这似乎是 outstanding bug 在映射的数组/元组中,映射的类型在任何地方都不被视为数组。这可能会在TypeScript3.1的发行版中得到修复。现在,您可以通过添加一个新的伪类型参数来解决问题,如

    type Prop<T, K> = K extends keyof T ? T[K] : never;
    type MappedArgs = {[K in keyof TArgs]: Prop<TProps,TArgs[K]>}
      & unknown[]; // definitely an array!
    type ConstructorType<A extends MappedArgs = MappedArgs> = new (...args: A) => any;
    

    这很管用。我们看看能不能测试一下。怎么样:

    type Prop<T, K> = K extends keyof T ? T[K] : never;
    interface NewMethod<TProps> {
      create<TArgs extends Array<Extract<keyof TProps, string>>,
        MTArgs extends unknown[] & { [K in keyof TArgs]: Prop<TProps, TArgs[K]> }>(
          geometryClass: new (...args: MTArgs) => any,
          ...args: Array<Extract<keyof TProps, string>>): void;
    }
    
    declare const z: NewMethod<{ a: string, b: number }>;
    z.create(null! as new (x: string, y: number) => any, "a", "b"); // okay
    z.create(null! as new (x: string, y: number) => any, "a", "c"); // error, "c" is bad
    z.create(null! as new (x: string, y: boolean) => any, "a", "b"); // error, constructor is bad
    

    他们的行为似乎是你想要的。。。尽管上一个案例中的错误非常模糊,似乎并没有指出问题在于 y 参数是 boolean 不匹配 string number TProps[keyof TProps] .


    不管怎么说,到2018年8月为止,这仍然是一件很有前途的事情,所以我认为你可能需要等一段时间,然后才能确定它到底会如何工作。希望能有所帮助。祝你好运!