在 TypeScript 中,当类型是函数的参数时,有没有办法限制 Partial<T> 类型的 extra/excess 属性?

In TypeScript, is there a way to restrict extra/excess properties for a Partial<T> type when the type is a parameter to a function?

有没有一种标准方法可以让场景 1 出现编译错误,因为没有指定已知属性,就像场景 2 一样?或者有什么解决方法吗?

class Class2 {
  g: number;
}

class Testing {
  static testIt3<T>(val: Partial<T>): void {
  }
}

const test = {
  g: 6,
  a: '6',
};

// Scenario 1
Testing.testIt3<Class2>(test);
// TS does not show any errors for this scenario

// Scenario 2
Testing.testIt3<Class2>({
  g: 6,
  a: '6',
});
// but it does for this scenario:
// Object literal may only specify known properties...

Live code

类型系统不适合对额外对象键的此类限制。 TypeScript 中的类型不是 exact:如果一个对象是 A 类型,并且你向它添加了更多 A 定义中未提及的属性,该对象仍然是类型A。这基本上是支持 class 继承所必需的,其中 subclasses 可以向 superclasses 添加属性。

唯一一次编译器将类型视为精确类型是在您使用 "fresh" 对象字面量(即尚未分配给任何对象的字面量)并将其传递给需要对象类型。这称为 excess property checking ,它是一种解决语言中缺少确切类型的方法。您希望对 "non-fresh" 对象(例如 test)进行额外的 属性 检查,但这不会发生。

TypeScript 没有精确类型的具体表示;您不能使用类型 T 并从中生成 Exact<T> 。但是您可以使用 generic constraint 来获得这种效果。给定一个类型 T,以及一个类型为 U 的对象,您希望它符合无法表示的 Exact<T> 类型,您可以这样:

type Exactly<T, U extends T> = {[K in keyof U]: K extends keyof T ? T[K] : never};
type IsExactly<T, U extends T> = U extends Exactly<T, U> ? true : false;

const testGood = {
    g: 1
}
type TestGood = IsExactly<Class2, typeof testGood>; // true

const testBad = {
    g: 6,
    a: '6',
};
type TestBad = IsExactly<Class2, typeof testBad>; // false

所以编译器能够分辨出 typeof testGood 是“Exactly<Class2, typeof testGood>”,而 typeof testBad 不是 Exactly<Class2, typeof testBad>。我们可以用它来构建一个通用函数来做你想做的事。 (在你的情况下,你想要 ExactlyPartial<T, U> 而不是 Exactly<T, U>,但它非常相似......只是不要限制 U 扩展 T)。


不幸的是,您的函数在 T 中已经是泛型了,类型要精确。而你手动指定T,泛型函数需要推断U的类型。 TypeScript 不允许 partial type parameter inference。您必须手动指定函数中的所有类型参数,或者必须让编译器推断函数中的所有类型参数。所以有解决方法:

一个是将您的函数拆分为 curried 函数,其中第一个泛型函数让您指定 T,returned 泛型函数推断 U .它看起来像这样:

    class Testing {
        static testIt<T>(): <U extends { [K in keyof U]:
            K extends keyof T ? T[K] : never
        }> (val: U) => void {
            return () => { }
        }
    }

    Testing.testIt<Class2>()(testBad); // error, prop "a" incompatible
    Testing.testIt<Class2>()(testGood); // okay

这按您预期的方式工作,但会影响运行时,因为您必须在运行时无缘无故地调用柯里化函数。

另一种解决方法是将一个值传递给函数,从中可以推断出 T。由于您不需要这样的值,因此这实际上是一个虚拟参数。同样,它对运行时有影响,因为您必须传入一个未使用的值。 (您提到您实际上可能在运行时使用这样的值,在这种情况下,这不再是一种解决方法,而是建议的解决方案,因为无论如何您都需要传递一些东西,并且 T 的手动规范在你的代码示例是一条红鲱鱼。)它看起来像这样:

    class Testing {
        static testIt<T, U extends { [K in keyof U]:
            K extends keyof T ? T[K] : never
        }>(ctor: new (...args: any) => T, val: U) {
            // not using ctor in here, so this is a dummy value                        
        }
    }

    Testing.testIt(Class2, testBad); // error, prop "a" incompatible
    Testing.testIt(Class2, testGood); // okay    

我能想到的第三个解决方法是只使用类型系统来表示柯里化函数 return 的结果而不实际调用它。它完全没有运行时影响,这使得它更易于为现有的 JS 代码提供类型,但使用起来有点笨拙,因为您必须断言 Testing.testIt 以正确的方式行事。它看起来像这样:

    interface TestIt<T> {
        <U extends { [K in keyof U]: K extends keyof T ? T[K] : never }>(val: U): void;
    }

    class Testing {
        static testIt(val: object) {
            // not using ctor in here, so this is a dummy value                        
        }
    }

    (Testing.testIt as TestIt<Class2>)(testBad); // error, prop "a" incompatible
    (Testing.testIt as TestIt<Class2>)(testGood); // okay

好的,希望其中一个对您有用。祝你好运!

Link to code