【问题标题】:Function return type that matches parameter types (when using the spread operator)?与参数类型匹配的函数返回类型(使用扩展运算符时)?
【发布时间】:2021-04-16 15:59:40
【问题描述】:

我可以为下面的Mixin() 函数定义一个返回类型,该函数将解析为任何参数类型的交集类型吗?

function Mixin(...classRefs: any[]) {
  return merge(class {}, ...classRefs);
}

function merge(derived: any, ...classRefs: any[]) {
  classRefs.forEach(classRef => {
    Object.getOwnPropertyNames(classRef.prototype).forEach(name => {
      if (name !== 'constructor') {
        Object.defineProperty(
          derived.prototype,
          name,
          Object.getOwnPropertyDescriptor(classRef.prototype, name) as PropertyDescriptor
        );
      }
    });
  });

  return derived;
}

class Foo {
  foo() {}
}

class Bar {
  bar() {}
}

class Baz {
  baz() {
    console.log('baz');
  }
}

class MyClass extends Mixin(Foo, Bar, Baz) {}

const my = new MyClass();
my.baz();

如果没有展开运算符,使用泛型会相当简单。如何确保类型系统匹配实际发生的情况?

编辑:我修改了示例以匹配我实际使用的 mixin 函数。

编辑:两个后续问题:

  1. 如何确保...classRefs 中的所有类都扩展了一些公共基类?假设我创建了abstract class Mergable {},然后只能将后代传递给merge
  2. 静态属性/函数呢?我添加了第二个循环来获取它们,但经过一些实验后,我不确定如何让类型系统意识到这一点:
function Mixin<T extends ClassType, R extends T[]>(...classRefs: [...R]):
    new (...args: any[]) => UnionToIntersection<InstanceType<[...R][number]>> {
    return merge(class { }, ...classRefs);
}

function merge(derived: ClassType, ...classRefs: ClassType[]) {
    classRefs.forEach(classRef => {
        Object.getOwnPropertyNames(classRef).forEach(name => {
          const descriptor = Object.getOwnPropertyDescriptor(classRef, name);

          if (name !== 'name' && name !== 'prototype' && name !== 'length' && descriptor) {
            Object.defineProperty(
              derived,
              name,
              descriptor
            )
          }
        });

        // Static Properties
        Object.getOwnPropertyNames(classRef).forEach(name => {
            // you can get rid of type casting in this way
            const descriptor = Object.getOwnPropertyDescriptor(classRef.prototype, name)
            if (name !== 'name' && name !== 'prototype' && name !== 'length' && descriptor) {
                Object.defineProperty(
                    derived.prototype,
                    name,
                    descriptor
                );
            }
        });

        // Instance Properties
        Object.getOwnPropertyNames(classRef.prototype).forEach(name => {
            // you can get rid of type casting in this way
            const descriptor = Object.getOwnPropertyDescriptor(classRef.prototype, name)
            if (name !== 'constructor' && descriptor) {
                Object.defineProperty(
                    derived.prototype,
                    name,
                    descriptor
                );
            }
        });
    });

    return derived;
}

【问题讨论】:

  • 我不明白。 The code as written 出现运行时错误; merged.baz() 在 JavaScript 中不起作用。大概通过“按预期工作”你的意思是别的......你能把它设为minimal reproducible example,所以它实际上在运行时工作吗?如果问题只是“你能输入一个输出是输入交集的函数吗”,答案是yes。但我不认为这是extend() 应该做的(因为构造签名的交集不构造实例的交集),也不是它在实践中所做的。救命!
  • 在我努力创建一个最小示例的过程中,我似乎弄坏了一些东西。将使用工作版本进行更新。

标签: typescript typescript-typings typescript-generics


【解决方案1】:

如果它适合你,请告诉我:

// credits goes to @jcalz
type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends (
        k: infer I
    ) => void
    ? I
    : never;

type Infer<T> =
    T extends infer R
    ? R extends new (...args: any) => any
    ? InstanceType<R>
    : never
    : never;

type ClassType = new (...args: any[]) => any;
function Mixin<T extends ClassType, R extends T[]>(...classRefs: [...R]):
    new (...args: any[]) => UnionToIntersection<InstanceType<[...R][number]>> {
    return merge(class { }, ...classRefs);
}

function merge(derived: ClassType, ...classRefs: ClassType[]) {
    classRefs.forEach(classRef => {
        Object.getOwnPropertyNames(classRef.prototype).forEach(name => {
            // you can get rid of type casting in this way
            const descriptor = Object.getOwnPropertyDescriptor(classRef.prototype, name)
            if (name !== 'constructor' && descriptor) {
                Object.defineProperty(
                    derived.prototype,
                    name,
                    descriptor
                );
            }
        });
    });

    return derived;
}

class Foo {
    foo() { }
}

class Bar {
    bar() { }
}

class Baz {
    baz() {
        console.log('baz');
    }
}

class MyClass extends Mixin(Foo, Bar, Baz) { }

const my = new MyClass();
my.foo() // ok
my.bar() // ok
my.baz(); // ok

Playground

推断类型

type Infer<T> = /*1*/ T extends infer R ? /*2*/ R extends new (...args: any) => any ? /*3*/ InstanceType<R> : never : never;
  1. 只是推断类型。 Docs
  2. 检查 R 是否扩展了 ClassType
  3. 如果 R has 是 ClassType,则返回 InstanceType&lt;R&gt;

混音

function Mixin</*1*/T extends ClassType, /*2*/R extends T[]>(...classRefs: /*3*/[...R]): new (...args: any[]) =>/*4*/ UnionToIntersection<InstanceType<[...R][number]>> {
    return merge(class { }, ...classRefs);
}
  1. 我已将T generic 定义为所有参数的基本类型。
  2. R 用于参数类型。如您所见,它是T 的数组,也就是类数组
  3. 使用[...R] 而不是T[] 很重要。因为T[],会期望所有参数都具有完全相同的类型,这在我们的例子中没有用。
  4. [...R][number]&gt; 表示数组中所有元素的联合。示例[1,2,3][number] === 1|2|3InstanceType&lt;[...R][number]&gt; 表示 InstanceType|InstanceType|InstanceType。而UnionToIntersection 只是将所有联合合并为一种类型。

更新 3

如何确保 ...classRefs 中的所有类都扩展了一些公共基类?假设我创建了抽象类 Mergable {},然后只能将后代传递给合并。

只需将Mixin 替换为下一个即可:

function Mixin<T extends ClassType, R extends T[]>(...classRefs: [T, ...R]):
    new (...args: any[]) => UnionToIntersection<InstanceType<[...R][number]>> {
    return merge(class { }, ...classRefs);
}

这里我用T 泛型类型明确定义了第一个元素。现在,TS 知道R 应该扩展T 的数组。

现在尝试使用Bar 类而不从Foo 扩展。你会收到一个错误

【讨论】:

  • 我想我正在关注这里发生的事情,但我注意到 Infer&lt;T&gt; 类型没有在任何地方使用,这是预期的吗?
  • 哦,对不起,这里的 Infer 是多余的
  • @Wilco 关于静态方法。你想确保所有类都有相同的静态方法吗?
  • 传递给merge()的每个类的静态方法不能不同,想法是它也会将它们复制到生成的合并类中。
  • 老实说,我从来没有遇到过这种情况。我不知道如何 copt 静态方法。
【解决方案2】:

您可以编写一个类型函数,该函数接受一个元组并生成其所有元素的intersection。显而易见的方法是使用recursive conditional types:

// obvious version:
type IntersectAll<T extends readonly any[]> = 
  T extends [infer F, ...infer R] ? F & IntersectAll<R> : unknown;

这可行,但如果您的元组长于约 20 个元素,则会达到递归限制。有一个less obvious versionconditional type inference 中使用逆变:

// less obvious version:
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends
  ((k: infer I) => void) ? I : never
type IntersectAll<T extends readonly any[]> = Extract<
  UnionToIntersection<
    { [K in keyof T]: [T[K]] }[number]
  >, readonly any[]>[number]

两个版本都应该在以下情况下工作:


您希望merge() 至少采用一个零参数的类构造函数(对吗?我不想担心如果您的构造函数需要参数来构造它们会发生什么),并生成一个新的类构造函数来生成原始构造函数的所有实例类型的交集。

如果是这样,我会这样打:

function merge<T, U extends any[]>(
  derived: new () => T,
  ...classRefs: { [I in keyof U]: new () => U[I] }
) {
  classRefs.forEach(classRef => {
    Object.getOwnPropertyNames(classRef.prototype).forEach(name => {
      if (name !== 'constructor') {
        Object.defineProperty(
          derived.prototype,
          name,
          Object.getOwnPropertyDescriptor(
            classRef.prototype, name) as PropertyDescriptor
        );
      }
    });
  });

  return derived as new () => (T & IntersectAll<U>);
}

本质上,这个函数就是T中的generic,第一个构造函数的实例类型;和U,来自其余构造函数的实例类型元组。所以derived 有一个construct signature new () =&gt; T,而classRefs 有一个构造签名元组,由mapped tuple 表示,你在其中获取U 的元素并用new () =&gt; ... 包装它。返回类型是new() =&gt; (T &amp; IntersectAll&lt;U&gt;),这是一个构造函数,它产生所有其他构造函数的交集。

Mixin() 也同样通用:

function Mixin<T extends any[]>(
  ...classRefs: { [I in keyof T]: new () => T[I] }
) {
  return merge<unknown, T>(class { }, ...classRefs);
}
/* function Mixin<T extends any[]>(
  ...classRefs: { [I in keyof T]: new () => T[I]; }
): new () => IntersectAll<T> */

You can verify that it works as expected:

class Foo { foo() { return 6; } }
class Bar { bar() { return "abc" } }
class Baz { baz() { console.log('baz'); } }
class MyClass extends Mixin(Foo, Bar, Baz) { }

const my = new MyClass();
console.log(my.foo().toFixed(2)); // "6.00"
console.log(my.bar().toUpperCase()); // "ABC"
my.baz(); // "baz"

你去。我假设这里有一些边缘情况,因为手动玩原型可以做一些有趣的事情。如果您将具有相同属性或方法名称的冲突类型的类混合在一起,则交集并不完全正确。当然,还有类构造函数参数。这种边缘情况可能是可以解决的,但会增加复杂性......但我认为其中任何一个都超出了这里的范围。

Playground link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多