【问题标题】:inference not working on type argument from a higher order function推理不适用于来自高阶函数的类型参数
【发布时间】:2020-12-31 12:14:11
【问题描述】:

我已经为这个问题精简了这段代码。这是playground

我有这个函数,它返回一个带有类型参数的函数:

export function createInteractor<E extends Element>(name: string) {
  return <S extends InteractorSpecification<E, S>>(specification: InteractorSpecification<E, S>) => {
    const result: unknown = {}
    return result as unknown as InteractorType<E, typeof specification>;
  }
}

InteractorSpecification 类型会查找一个名为 locators 的属性,该属性具有动态且由用户添加的字段

export type InteractorSpecification<E extends Element, S extends InteractorSpecification<E, S>> = 
  {
    selector?: string;
    locators: Record<keyof S['locators'], LocatorFn<E>>;
  };

高阶函数返回此类型

return result as unknown as InteractorType<E, typeof specification>;

InteractorType 看起来像这样:

export type LocatorImplementation<E extends Element, S extends InteractorSpecification<E, S>> = 
  {[K in keyof S['locators']]: (value: string) => InteractorInstance<E, S>}

export type InteractorType<E extends Element, S extends InteractorSpecification<E, S>> = LocatorImplementation<E, S>;

基本上它从原始specificationlocation 中获取属性并将它们映射到返回类型。

没有推断出高阶函数的类型参数,但如果我传入显式类型参数,它就可以工作:

const Link = createInteractor<HTMLLinkElement>('link')({
  selector: 'a',
  locators: {
    byThis: (element) => element.href,
    byThat: (element) => element.title
  },
});

// type is not inferred 
// const Link: LocatorImplementation<HTMLLinkElement, InteractorSpecification<HTMLLinkElement, InteractorSpecification<HTMLLinkElement, unknown>>>
Link.byThat('foo');
Link.byThis('bar')


const spec = {
  selector: 'a',
  locators: {
    byThis: (element: HTMLLinkElement) => element.href,
    byThat: (element: HTMLLinkElement) => element.title
  },
  asfsdfsd: 'I also need to catch unknown props'
}

// works with explicit type
const F = createInteractor<HTMLLinkElement>('link')<typeof spec>(spec)

F.byThat('foo');
F.byThis('bar');

【问题讨论】:

    标签: typescript


    【解决方案1】:

    推理失败源于createInteractor,是泛型使用不当造成的。

    function createInteractor<E extends Element>(name: string) {
      return <S extends InteractorSpecification<E, S>>(specification: InteractorSpecification<E, S>) => {
        const result: unknown = {}
        return result as unknown as InteractorType<E, typeof specification>;
      }
    }
    

    内部函数声明S 将其类型与外部函数中E 的类型相关联。这是正确且可取的,但问题是S 没有在可以推断的上下文中使用。

    为了解决这个问题,我们将内部函数更改为

    <S extends InteractorSpecification<E, S>>(specification: S) => {...}
    

    这强制执行相同的约束,但将 S 与值 specification 相关联,这样 S 是从在调用站点为 specification 传递的参数推断出来的,并且关键的是,S 在其约束 extends InteractorSpecification&lt;E, S&gt; 中也是从该参数推断出来的,最终确定 locator 和定位器类型。

    虽然这一更改足以解决您遇到的特定问题,但createInteractor 中的泛型再次引起了相关问题,但这次是在类型参数E 中。

    当你有一个类型参数不涉及任何函数参数的类型时,你应该做一个双重考虑。它通常是伪装成类型参数的类型断言。

    在我们的具体情况下,它允许我们编写类似的代码

    const F = createInteractor<HTMLLinkElement>('input')(spec)
    

    即使规范应该适用于HtmlLinkElements。

    TypeScript 的内置 DOM 类型在 lib.dom.d.ts 中提供了一个有用的类型,称为 HtmlElementTagNameMap,它将标签名称映射到它们对应的元素类型(这是 document.querySelector('input')?.value 类型检查的方式)。

    我们将使用这些信息来防止此类无效调用,同时进一步改善消费体验,通过获取特定的标签名称并使用它来确定和传播元素类型。

    function createInteractor<N extends keyof HTMLElementTagNameMap>(name: N) {
      type Tag = HTMLElementTagNameMap[N];
      return <S extends InteractorSpecification<Tag, S>>(specification: S) => {
        return {} as InteractorType<Tag, S>;
      }
    }
    

    因此我们现在可以写

    const Link = createInteractor('link')({
      selector: 'a',
      locators: {
        byThis: (element) => element.href,
        byThat: (element) => element.title
      },
    });
    
    // type is inferred 
    
    Link.byThat('foo');
    Link.byThis('bar')
    

    安全简洁,而我们现在希望收到一个错误

    const badSpec = {
      selector: 'a',
      locators: {
        byThis: (element: HTMLInputElement) => element.value, 
        byThat: (element: HTMLLinkElement) => element.title
      },
    }
    // error is correctly given for incompatible locator 
    const bad = createInteractor('link')(badSpec);
    

    Playground Link

    【讨论】:

    • 感谢您的回答。 createInteractorname 参数实际上与元素类型无关。在实际代码中,它只是用于调试,所以它可以是任何东西。原始代码实际上是&lt;S extends InteractorSpecification&lt;Tag, S&gt;&gt;(specification: S) 但这意味着任何键都可以传递给so:``` const Link = createInteractor('link')({ selector: 'a', locators: { byThis: (element) => element.href, }, sdfds: 'aaa' }); ``` 对属性sdfds 有效,这就是我将其更改为具体类型的原因。
    • 我明白了,对此感到抱歉。但是,我建议您为标签名称引入第二个参数,或者将标签名称设为 specification 类型的属性,这样您就不必传递显式泛型。
    猜你喜欢
    • 2020-01-24
    • 2012-11-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多