简体   繁体   English

泛型参数上的类型扩展失去类型安全性

[英]type widening on generic parameter losing type safety

I have created this playground with this code我用这段代码创建了这个游乐场

export type Spec<E extends Element> = {
  prop?: string;
  getters?: Record<string, (element: E) => string>;
}

export class Thing<E extends Element, S extends Spec<E>> {
  constructor(
    public name: string,
    private specification: S,
  ) {}
}

export type InteractorInstance<E extends Element, S extends Spec<E>> = Thing<E, S>;

export type GetterImplementation<E extends Element, S extends Spec<E>> = {
  [P in keyof S['getters']]: S['getters'][P] extends ((element: E, ...args: unknown[]) => unknown) ? (value: string) => InteractorInstance<E, S> : never;
}

export type InteractorType<E extends Element, S extends Spec<E>> =
  ((value: string) => InteractorInstance<E, S>) &
  GetterImplementation<E, S>;


export function createThing<E extends Element>(name: string) {
  return function<S extends Spec<E>>(specification: S) {
    const result = function(value: string): Thing<E, S> {
      let thing = new Thing<E, S>(name, specification);
      return thing;
    }

    return result as InteractorType<E, S>;
  }
}

const Link = createThing<HTMLLinkElement>('link')({
  prop: 'a',
  getters: {
    byThis: (element) => element.href,
    byThat: (element) => element.title
  },
  whoCaresWhatThisIs: 666. // should not be here
});

// THis is why Spec needs to be a generic type argument
// I do some shenanigans in LocatorImplementation to add these props onto the Thing
Link.byThat('bb');
Link.byThis('cc')

Can I make the Spec only have the keys of Spec , ie anything other than prop or getters is invalid?我可以让Spec只有Spec的键,即除了propgetters之外的任何东西都是无效的?

I need Spec to be a type argument because it is used in a conditional type in SpecImplementation我需要 Spec 作为类型参数,因为它在SpecImplementation的条件类型中使用

As pointed out earlier, S extends Spec allows extra properties - exactly what you want to avoid!如前所述, S extends Spec允许额外的属性——正是你想要避免的!

But let's see why you need to extend Spec exactly... Looks like you need you be able to provide a custom map of getters, while in Spec it's defined as Record<string, Function> .但是让我们看看为什么您需要完全扩展 Spec ......看起来您需要能够提供自定义 map 的 getter,而在Spec中它被定义为Record<string, Function> Ok - so isn't it the clue then?好的-那不是线索吗? Let's make Spec itself generic, parameterized by the keys in this record:让我们使 Spec 本身成为通用的,由该记录中的键参数化:

export type Spec<E extends Element, G extends string> = {
  prop?: string;
  getters?: Record<G, (element: E) => string>;
}

Then we update all derived types correspondingly.然后我们相应地更新所有派生类型。 Note that GetterImplementation mapped type becomes simpler now, as it works directly with the new generic parameter:请注意, GetterImplementation映射类型现在变得更简单,因为它直接与新的泛型参数一起使用:

export type GetterImplementation<E extends Element, G extends string> = {
  [P in G]: (value: string) => InteractorInstance<E, G>;
}

(in fact I might have misunderstood the intention with this mapped type... if you do want smart handling of getters, it should be made a part of Spec - currently Spec doesn't allow to have a getter with extra arguments). (事实上,我可能误解了这种映射类型的意图......如果你确实想要智能处理 getter,它应该成为Spec的一部分——目前Spec不允许有一个带有额外参数的 getter)。

And finally, the function itself is gonna be generic with the new G extends string generic parameter instead of Spec :最后,function 本身将是通用的,使用新的G extends string通用参数而不是Spec

export function createThing<E extends Element>(name: string) {
  return function<G extends string>(specification: Spec<E, G>) {
    const result = function(value: string): Thing<E, G> {
      let thing = new Thing<E, G>(name, specification);
      return thing;
    }

    return result as InteractorType<E, G>;
  }
}

Playground 操场

If Spec needs to be strongly typed, then why S extends Spec?如果 Spec 需要强类型,那为什么 S 扩展 Spec 呢? I tried the following and the returned type didn't allow anything apart from props and getters,我尝试了以下方法,返回的类型除了道具和吸气剂之外不允许任何东西,

export function createThing<E extends Element>(name: string) {
  return function(specification: Spec<E>) {
    const result = function(value: string): Thing<E, Spec<E>> {
      let thing = new Thing<E, Spec<E>>(name, specification);
      return thing;
    }

    return result;
  }
}

Checkout my forked playground here: Playground Link在这里查看我的分叉游乐场: 游乐场链接

I made some change to your code in playground that I think solves your problem.在操场上对您的代码进行了一些更改,我认为可以解决您的问题。

In your original code yuo had S extends Spec<E> as an type argument you passed around.在您的原始代码中,您将S extends Spec<E>作为您传递的类型参数。 This meant that Type S can allow extra properties.这意味着 Type S可以允许额外的属性。

However, by defining a Type argument T extends string which you pass around, your function argument is of type Spec<E, T> now instead of S extends Spec<E> which means it can't allow extra properties.但是,通过定义您传递的 Type 参数T extends string ,您的 function 参数现在是Spec<E, T>类型而不是S extends Spec<E> ,这意味着它不能允许额外的属性。

I include the modified code here as well:我在这里也包含了修改后的代码:

export type Spec<E extends Element, T extends string> = {
  prop?: string;
  getters?: {
    [key in T]: (element: E) => string;  
  }
}

export class Thing<E extends Element, T extends string> {
  constructor(
    public name: string,
    private specification: Spec<E, T>,
  ) {}
}

export type InteractorInstance<E extends Element, T extends string> = Thing<E, T>;

// GetterImplementation is simpler now. 
export type GetterImplementation<E extends Element, T extends string> = {
  [P in T]: (value: string) => InteractorInstance<E, T>;
}

export type InteractorType<E extends Element, T extends string> =
  ((value: string) => InteractorInstance<E, T>) &
  GetterImplementation<E, T>;


export function createThing<E extends Element>(name: string) {
  return function<T extends string>(specification: Spec<E, T>) {
    const result = function(value: string): Thing<E, T> {
      let thing = new Thing<E, T>(name, specification);
      return thing;
    }

    return result as InteractorType<E, T>;
  }
}

// Type T here is infered as 'byThis'|'byThat'
const Link = createThing<HTMLLinkElement>('link')({
  prop: 'a',
  getters: {
    byThis: (element) => element.href,
    byThat: (element) => element.title,
  },
  whoCaresWhatThisIs: 666. // This now gives an error as desired
});

// THis is why Spec needs to be a generic type argument
// I do some shenanigans in LocatorImplementation to add these props onto the Thing
Link.byThat('bb'); // This work as expected now
Link.byThis('cc'); // This work as expected now
// Since Link is of Type InteractorType<HTMLLinkElement, 'byThat'|'byThis'>, byThose does not work
Link.byThose('dd'); // This also results in an error

I hope my answer solves your problem, and thank you for the question.希望我的回答能解决你的问题,谢谢你的提问。 Thinking about the solution helped learn something new.思考解决方案有助于学习新知识。

As far as I understand it, TS doesn't support excess property checks for generic parameter object literals.据我了解,TS 不支持对通用参数 object 文字进行过多的属性检查。 I think what you're looking for is exact types - something that's in discussion here: https://github.com/microsoft/TypeScript/issues/12936我认为您正在寻找的是确切的类型 - 这里正在讨论的内容: https://github.com/microsoft/TypeScript/issues/12936

What is possible is something a bit hacky on the basis that you can't assign a parameter which has a type never to a function.基于您无法将类型永远不会分配给 function 的参数,可能会有点 hacky。 So here I check the generic to see if it meets the criteria, and then block if it isn't.所以在这里我检查泛型,看看它是否符合标准,如果不符合则阻止。 The error message isn't the prettiest though.错误消息不是最漂亮的。


type EnforceKeyMatch<I extends {[KI in keyof T]: any }, T extends {[TI in keyof T]?: any }> = keyof I extends keyof T ? keyof T extends keyof I ? I : never : never;

export function createThing<E extends Element>(name: string) {
  return function<S extends Spec<E>>(specification: EnforceKeyMatch<S, Spec<Element>>) {
    const result = function(value: string): Thing<E, S> {
      let thing = new Thing<E, S>(name, specification);
      return thing;
    }

    return result as InteractorType<E, S>;
  }
}

With this I try to enforce that for the params to EnforceKeyMatch, the keys of I (input) extend the keys of T (target), and vice versa.有了这个,我尝试强制执行 EnforceKeyMatch 的参数,I(输入)的键扩展 T(目标)的键,反之亦然。 This then spits out a never type if this condition is not met for specification.如果规范不满足此条件,则它会吐出 never 类型。

So here with any extra property the type now errors:所以这里有任何额外的属性类型现在错误:

在此处输入图像描述

But with the property removed it functions as normal:但是删除该属性后,它可以正常工作:

在此处输入图像描述

Here is a link to the playground 这是游乐场的链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM