Typescript - 如何在switch语句中缩小泛型类型的可能性?

7

我正在尝试编写一个函数,根据传递的键和参数执行特定的计算。我还想强制传递的键和参数之间的关系,因此我使用了带有约束的通用函数:

interface ProductMap {
  one: {
    basePrice: number;
    discount: number;
  },
  two: {
    basePrice: number;
    addOnPrice: number;
  }
}

function getPrice<K extends keyof ProductMap>(key: K, params: ProductMap[K]) {
  switch (key) {
    case 'one': {
      return params.basePrice - params.discount; // Property 'discount' does not exist on type 'ProductMap[K]'.
    }
    case 'two': {
      return params.basePrice + params.addOnPrice;
    }
  }
}

也许我对此考虑得不够全面,但似乎Typescript应该能够在switch语句中缩小泛型类型范围。唯一使它发挥作用的方法是采用这种笨拙的方式:

function getPrice<K extends keyof ProductMap>(key: K, params: ProductMap[K]) {
  switch (key) {
    case 'one': {
      const p = params as ProductMap['one'];
      return p.basePrice - p.discount;
    }
    case 'two': {
      const p = params as ProductMap['two'];
      return p.basePrice + p.addOnPrice;
    }
  }
}

有人能解释一下为什么 #1 不起作用或者提供一个替代方案吗?


请参阅microsoft/TypeScript#13995了解为什么这不起作用。您的类型断言解决方案可能是处理此问题最合理的方式。 - jcalz
2
这个链接似乎是一个合理的选择吗?它没有使用断言,所以更“安全”。 - jcalz
3个回答

6

"有人能解释为什么#1行不通或提供替代方案吗?"

以下是为什么#1不起作用的原因:Typescript对于像key这样的变量具有控制流类型缩小,但对于像K这样的类型参数却没有。

case'one':检查将变量key:K的类型缩小为key:'one'

但它并没有从K extends 'one'|'two'缩小到K extends 'one',因为对实际类型变量K没有进行任何测试,也无法进行任何测试来缩小它。因此,params:ProductMap[K]仍然是params:ProductMap[K],而K仍然是相同的类型,因此params的类型没有被缩小。


这里有一个替代解决方案:使用带标签的联合类型,并根据鉴别器(即下面代码中的__tag属性)进行切换。
type ProductMap =
  {
    __tag: 'one';
    basePrice: number;
    discount: number;
  } | {
    __tag: 'two';
    basePrice: number;
    addOnPrice: number;
  }

function getPrice(params: ProductMap): number {
  switch (params.__tag) {
    case 'one': {
      return params.basePrice - params.discount;
    }
    case 'two': {
      return params.basePrice + params.addOnPrice;
    }
  }
}

游乐场链接


当然,这个问题在于,如果你想要基于每个判别式返回不同的返回类型,那么返回类型将是所有可能返回类型的联合,而不是对应于实际传递的判别式的那个,就像泛型所做的那样。 - Madara's Ghost
这种方法并不是问题;它是一个不同但相关的问题,答案更加复杂。您仍然可以使用带标记联合并编写适当的通用签名,以允许在某些调用点推断出更具体的返回类型。 - kaya3

1

实际上,TypeScript 看起来并不那么聪明,但有一种更好的解决方法,比强制类型转换更好:

function getPrice(productMap: ProductMap, key: keyof ProductMap) {
  switch (key) {
    case 'one': {
      const params = productMap['one'];
      return params.basePrice - params.discount;
    }
    case 'two': {
      const params = productMap['two'];
      return params.basePrice + params.addOnPrice;
    }
  }
}

0

将特定类型转换是一种解决方案(但不是最佳方案):

interface ProductMap {
  one: {
    basePrice: number;
    discount: number;
  };
  two: {
    basePrice: number;
    addOnPrice: number;
  };
}

function getPrice<K extends keyof ProductMap>(
  key: K,
  _params: ProductMap[K]
) {
  switch (key) {
    case 'one': {
      const params = _params as ProductMap['one'];
      return params.basePrice - params.discount;
    }
    case 'two': {
      const params = _params as ProductMap['two'];
      return params.basePrice + params.addOnPrice;
    }
  }
}

为了保持单一的返回类型,定义函数返回类型

interface ProductMap {
  one: {
    basePrice: number;
    discount: number;
  };
  two: {
    basePrice: number;
    addOnPrice: number;
  };
}

function getPrice<K extends keyof ProductMap>(
  key: K,
  _params: ProductMap[K]
): number {
  switch (key) {
    case 'one': {
      const params = _params as ProductMap['one'];
      return params.basePrice - params.discount;
    }
    case 'two': {
      const params = _params as ProductMap['two'];
      return params.basePrice + params.addOnPrice;
    }
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接