使用任何类型的参数键入一个函数,除非该函数的成员不是字符串

时间:2018-11-01 01:34:17

标签: typescript

我需要一个函数来接受具有以下限制的参数:

  1. 可以是任何东西(基元,对象,数组等),只要它没有具有名为x的成员即可。
  2. 如果确实有一个名为x的成员,则它的类型必须为string

如何键入类似的功能?

declare function foo<T /* extends ??? */ >(arg: T): boolean

使用条件类型,我可以正常工作,但是又出现了另一个问题。

type Constrained<T> = 'x' extends keyof T
    ? (T extends { x: string } ? T : never)
    : T;

declare function foo<T>(a: Constrained<T>): boolean;

基本上,Constraint<T>解析为never,如果T的成员名为x而不是string类型或解析为T除此以外。然后,对带有“无效”对象的foo的任何调用都将被拒绝,因为不能为never分配任何内容(never本身除外)。

效果很好...直到我遇到类似的东西

class SomeClass<U /* extends ??? */> {
    prop!: U;

    method() {
        // Fails :(
        // How to restrict U to allow this call?
        foo(this.prop); // <-- Error: Argument of type 'U' is not
                        //            assignable to parameter of
                        //            type 'Constrained<U>'.
    }
}

playground

1 个答案:

答案 0 :(得分:1)

除非您确实需要,否则我将倾向于避免此类复杂的约束。我的建议是从更原始的片段中构建您正在谈论的类型,例如:

type Unknown = string | number | boolean | symbol | null | void | object;
type Constraint = Exclude<Unknown, object> | { [k: string]: unknown, x?: string };

那个Constraint只是一个普通的旧联合,它或多或少地代表了除string键的值非x以外的所有对象。完美吗?也许不是,但是处理起来要容易得多

declare function foo<T extends Constraint>(a: T): boolean;
class SomeClass<T extends Constraint> { /* ... */ };

foo(undefined); // okay
foo(null); // okay
foo("string"); // okay
foo(123); // okay
foo([]); // okay
foo([123]); // okay
foo([123, { x: "string" }]); // okay
foo(() => 123); // okay
foo({}); // okay
foo({ a: 123 }); // okay
foo({ a: 123, x: 123 }); // error
foo({ a: 123, x: { y: 123 } }); // error
foo(Math.random() < 0.5 ? 1 : { a: 123, x: "string" }); // okay

而且您在SomeClass内部也再没有问题了:

class SomeClass<T extends Constraint> {
  prop!: T;
  method() {
    foo(this.prop); // easy-peasy
  }
}


如果您迫切需要循环约束或自引用约束,则可以安抚编译器,但这对我来说是一个反复试验的事情,并且一路走来也有很多陷阱。让我们从您的类型函数开始:

type Constrained<T> = 'x' extends keyof T
  ? (T extends { x: string } ? T : never)
  : T;

您最初的foo定义似乎起作用,但只能通过一些可能引起麻烦的type inference

declare function foo<T>(a: Constrained<T>): boolean;

T类型为a的情况下,编译器如何知道Constrained<T>?它必须通过以某种方式查看条件类型,将Constrained<T>视为T的推理站点。我猜测,编译器看到Constrained<T>可分配给never | T的{​​{1}},因此推断T与{ {1}}。无论如何,那很好。


做这种事情的一种更“官方支持”的方法是使T的类型为a,因为交集是known to serve as inference sites。这与您所说的完全一样,但会让我在晚上睡得更香:

a

对于类,您真正想做的事情会给您一个循环约束错误:

T & Constrained<T>

这可以通过添加一些伪类型参数并使用条件类型来解决,该条件类型的评估将推迟到具体实例化declare function foo<T>(a: T & Constrained<T>): boolean; 之前:

class SomeClass<T extends Constrained<T>> { /* ... * / } // error!
// Type parameter 'T' has a circular constraint.

编译器不再注意到圆度,但是它仍然存在。

该类的实现仍然会给您错误,恰恰是因为您正在延迟编译器对否则为圆形约束的检查,因此它不知道您在做什么将是安全的:

SomeClass

一种处理方法是使class SomeClass<T extends (U extends any ? Constrained<T> : unknown), U = any> { /* ... * / } declare const ok: SomeClass<{ a: string }>; // okay declare const alsoOk: SomeClass<{ x: string }>; // okay declare const notOk: SomeClass<{ x: number }>; // error, number not a string 的类型为class SomeClass<T extends (U extends any ? Constrained<T> : unknown), U = any> { prop!: T; method() { foo(this.prop); // still error } } 而不是prop

Constrained<T>

但是您仍然可能会在其他地方遇到其他此类问题,并且您最终可能只需要使用type assertions来使错误消失:

T

无论如何,您可以看到这真是一团糟。这就是为什么我仍然推荐最初的旧联盟解决方案的原因。

好的,希望能有所帮助。祝你好运!