TypeScript 允许Map类型声明函数

o0lyfsai  于 6个月前  发布在  TypeScript
关注(0)|答案(9)|浏览(77)

建议

🔍 搜索词

  • mapped types
  • 示例成员函数
  • 示例成员属性
  • 示例方法
  • 子类构造函数
  • 覆盖

✅ 可实现性检查清单

我的建议符合以下指导原则:

  • 这不会对现有的TypeScript/JavaScript代码造成破坏性的改变
  • 这不会改变现有JavaScript代码的运行时行为
  • 这可以在不根据表达式的类型发出不同的JS的情况下实现
  • 这不是一个运行时特性(例如库功能、带有JavaScript输出的非ECMAScript语法、新的JS语法糖等)
  • 这个特性会与TypeScript's Design Goals一致。

⭐ 建议

📃 激励示例

在我们的世界中,有两种动物,狗和猫。

type Animal = 'dog' | 'cat';

有些人可以处理狗。有些人可以处理猫。有些人甚至可以同时处理两者。但如果你不能处理狗或猫,你就不是个处理者。我们可以用TypeScript来通用地表示这个概念。

type Handler<A extends Animal> = {[animal in A as `handle${Capitalize<animal>}`]: (animal: animal) => void};

假设你有一个处理狗的处理者,但它什么都不做。

class BoringDogHandler implements Handler<'dog'> {
	handleDog(dog : `dog`) {}
}

你也可以让一个人同时处理两者,并记录他处理了多少只动物。

class CountingUniversalAnimalHandler implements Handler<Animal> {
	count = 0;

	handleDog() {this.count++;}
	handleCat() {this.count++;}
}

有些处理狗的人在处理狗时会广播给全世界。由于这是很常见的事情,我们希望为此创建一个混入。

function NoisyDogHandler<H extends {new (...args: any[]): Handler<'dog'>}>(base: H) {
	return class extends base {
		handleDog(dog : `dog`) {
			console.log("Handling dog");
			return super.handleDog(dog);
		}
	};
}

看起来无害。但这不起作用!Typescript告诉我们:
Class Handler<"dog">定义了示例成员属性handleDog,但扩展的类(Anonymous class)将其定义为示例成员函数。(2425)
如果我们被允许声明handle${Capitalize<A>}成员不仅仅是属性,而实际上是方法,Typescript就不会害怕让我们覆盖它们。自然的语法应该是这样的,但目前还不允许:

type Handler<A extends Animal> = {[animal in A as `handle${Capitalize<animal>}`](animal: animal): void};

微妙的区别对于微妙的区别来说。
Playground Example

💻 用例

你打算用这个做什么?

混入很有力量,当它们有效时,它们非常出色,但它们目前在处理上有点脆弱。
这是将Map类型的构造函数扩展到覆盖方法所需的拼图的一部分( #27689 )。另一个需要的部分是能够保留从类型索引获取的方法的成员函数状态( #38496 ,参考 #35416#46802 )。

当前方法的缺点是什么?

对于足够小的用例,你可以“手动”进行Map:“”显然,如果你创建了多个使用 Animal 而不是只有一个的方法,或者 Animal 有更多情况,这种方法就无法很好地扩展。不太明显地,没有一个真正令人满意的方法来实现一个接受动物和处理者的泛型方法,调用正确的处理方法,确保编译时处理者可以处理动物,并且不涉及类型转换。请参阅此playground尝试几个方法。

uqjltbpv

uqjltbpv1#

根据用例,我倾向于在通过Map类型创建基类属性时丢弃 ... defines instance member property ..., but extended class ... defines it as instance member function. 错误。大家有什么想法?

csbfibhn

csbfibhn2#

请记住,假设这个提议的语法确实创建了方法,TypeScript可能会将它们双向转换,这意味着您可能不安全地将一个 CatHandler 分配给一个 DogAndCatHandler(我提出这一点只是因为预期的处理程序子类型关系在问题中明确提到)。
请参阅 https://github.com/microsoft/TypeScript/wiki/FAQ#why-are-function-parameters-bivariant 并注意,strictFunctionTypes 为函数类型启用了协变参数,但对于方法则没有。

e3bfsja2

e3bfsja23#

基于用例,我倾向于在通过Map类型创建基类属性时丢弃 ... defines instance member property ..., but extended class ... defines it as instance member function. 错误。你怎么看?
这对我来说听起来很合理。

hec6srdp

hec6srdp4#

哦,但是如果你确实要丢弃错误信息,确保它也为子类丢弃了。

kzmpq1sx

kzmpq1sx5#

处理这个问题也很好。消除这个情况下的错误听起来不错。我甚至可以说,任何时候示例成员属性都可以是单个方法签名(或未定义),那么它应该可以被重写,就像它是一个方法一样。

gt0wga4j

gt0wga4j6#

是的,如果这不是一个错误就好了。
我正在创建一个工具来推断来自一些遗留代码的类类型,这些遗留代码创建了自定义的ES6兼容类。
这个工具使用了Map类型,这个错误意味着在覆盖这些推断类中的ES6类的方法时,需要使用很多ts-ignores。

b5lpy0ml

b5lpy0ml7#

有同样的问题。

vmpqdwk3

vmpqdwk38#

我也有同样的问题👍

jrcvhitl

jrcvhitl9#

这是我的案例

export type AbstractModuleService<
  TContainer,
  TMainModelDTO,
  TOtherModelNamesAndAssociatedDTO extends OtherModelsConfigTemplate
> = AbstractModuleServiceBase<TContainer, TMainModelDTO> & {
  [K in keyof TOtherModelNamesAndAssociatedDTO as `retrieve${ExtractSingularName<
    TOtherModelNamesAndAssociatedDTO,
    K
  > &
    string}`]: (
    id: string,
    config?: FindConfig<any>,
    sharedContext?: Context
  ) => Promise<TOtherModelNamesAndAssociatedDTO[K & string]["dto"]>
} & {
  [K in keyof TOtherModelNamesAndAssociatedDTO as `list${ExtractPluralName<
    TOtherModelNamesAndAssociatedDTO,
    K
  > &
    string}`]: (
    filters?: any,
    config?: FindConfig<any>,
    sharedContext?: Context
  ) => Promise<TOtherModelNamesAndAssociatedDTO[K & string]["dto"][]>
} & {
  [K in keyof TOtherModelNamesAndAssociatedDTO as `listAndCount${ExtractPluralName<
    TOtherModelNamesAndAssociatedDTO,
    K
  > &
    string}`]: {
    (filters?: any, config?: FindConfig<any>, sharedContext?: Context): Promise<
      [TOtherModelNamesAndAssociatedDTO[K & string]["dto"][], number]
    >
  }
} & {
  [K in keyof TOtherModelNamesAndAssociatedDTO as `delete${ExtractPluralName<
    TOtherModelNamesAndAssociatedDTO,
    K
  > &
    string}`]: {
    (
      primaryKeyValues: string[] | object[],
      sharedContext?: Context
    ): Promise<void>
  }
} & {
  [K in keyof TOtherModelNamesAndAssociatedDTO as `softDelete${ExtractPluralName<
    TOtherModelNamesAndAssociatedDTO,
    K
  > &
    string}`]: {
    <TReturnableLinkableKeys extends string>(
      primaryKeyValues: string[] | object[],
      config?: SoftDeleteReturn<TReturnableLinkableKeys>,
      sharedContext?: Context
    ): Promise<Record<string, string[]> | void>
  }
} & {
  [K in keyof TOtherModelNamesAndAssociatedDTO as `restore${ExtractPluralName<
    TOtherModelNamesAndAssociatedDTO,
    K
  > &
    string}`]: {
    <TReturnableLinkableKeys extends string>(
      productIds: string[] | object[],
      config?: RestoreReturn<TReturnableLinkableKeys>,
      sharedContext?: Context
    ): Promise<Record<string, string[]> | void>
  }
}

我期望Map类型是类的函数,而不是属性,因为目前只能这样定义它们。目标是允许从子类中经典地覆盖该方法。但是由于属性(存在限制),我找到的绕过它的唯一方法是像下面这样重写它

@InjectManager("baseRepository_")
  async listVariants_(
    filters: ProductTypes.FilterableProductVariantProps = {},
    config: FindConfig<ProductTypes.ProductVariantDTO> = {},
    @MedusaContext() sharedContext: Context = {}
  ): Promise<ProductTypes.ProductVariantDTO[]> {
    const variants = await this.productVariantService_.list(
      filters,
      config,
      sharedContext
    )

    return JSON.parse(JSON.stringify(variants))
  }

  get listVariants() {
    return this.listVariants_
  }

关于这个主题有任何新进展吗?

相关问题