代码之家  ›  专栏  ›  技术社区  ›  Daniel

键入具有任何类型参数的函数,除非其成员不是字符串

  •  1
  • Daniel  · 技术社区  · 6 年前

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

    1. 可以是任何东西(原语、对象、数组等等),只要它 没有 x .
    2. 如果它 有一个叫 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 那不是那种类型 一串 或决心 否则。那么,有人打电话给我吗 foo (除 从未 本身)。

    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 回复  |  直到 6 年前
        1
  •  4
  •   jcalz    6 年前

    我倾向于远离像这样复杂的约束,除非你真的需要它。我的建议是从更原始的作品中建立你所说的类型,比如:

    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> ? 它必须治疗 T ,通过某种方式看穿条件类型。我 猜测 编译器看到了 约束<T> 可分配给 never | T ,由此推断 类型与 A. . 不管怎样,那很好。


    A. 类型 T & Constrained<T> known to serve as inference sites . 这和你说的完全一样,但会让我晚上睡得更香:

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

    至于上课,你真正关心的是 to do会产生循环约束错误:

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

    这可以通过添加一些伪类型参数和使用一个条件类型来解决,条件类型的计算将推迟到 某类 具体来说:

    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 类型 约束<T> 而不是 :

    class SomeClass<T extends (U extends any ? Constrained<T> : unknown), U = any> {
      prop!: Constrained<T>;
      method() {
        foo(this.prop); // okay now
      }
    }
    

    但你仍有可能在其他地方遇到其他类似的问题,最终你可能不得不随波逐流 type assertions 要消除错误:

    class SomeClass<T extends (U extends any ? Constrained<T> : unknown), U = any> {
      prop!: T;
      method() {
        foo(this.prop as Constrained<T>); // I know what I'm doing!
      }
    }
    

    不管怎样,你可以看到这是多么混乱。这就是为什么我仍然推荐最初的普通老工会解决方案。

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