Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

TS infers "never" type because it can't grok assignment in forEach loop

The error reads Property 'id' does not exist on type 'never'.

I understand enough about TypeScript to have thought that Control Flow Analysis would have me covered in this scenario. When I declare:

let quantityRange: QuantityLevels | false = false;

And, while iterating through one of the function parameters, a QuantityLevels typed variable may be assigned:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

priceList.quantityLevels.forEach((ql) => {
  if (ql.min <= quantity && ql.max >= quantity) {
    quantityRange = ql;
  }
});

When we eliminate the possibility of false being the value assigned to quantityRange for the remainder of the function by throwing an error in that case. However, TypeScript infers that the variable quantityRange could not have been assigned anything other than false, and is typed as never for the remainder of the function.

When I try to use properties of quantityRange, I’m rebuffed:

const priceByProduct = priceList.prices[product.id][quantityRange.id].price;
// error: Property 'id' does not exist on type 'never'.

What’s going on here? If I make an assignment of

quantityRange = priceList.quantityLevels[0];

(priceList.quantityLevels being typed as Array<QuantityLevels>), then all is well. This leaves me thinking that I must have an incomplete model of understanding when it comes to the way TypeScript deals with function/block-scoped variables. I’ve read through what I think are the relevant sections of the TypeScript handbook and have come up empty-handed (or perhaps empty-headed).

The code won’t pass a build action, but in the development and test environments, it’s working as expected.

The code below also rendered in TypeScript playground, along with errors.

function getPrice({ variantID, quantity, priceList }: GetPriceProps) {
  let quantityRange: QuantityLevels | false = false;

  priceList.quantityLevels.forEach((ql) => {
    if (ql.min <= quantity && ql.max >= quantity) {
      quantityRange = ql;
    }
  });

  // quantityRange = priceList.quantityLevels[0];
  // ^ Manually assigning this (which is what's happening in the above for loop) 
  //   makes TS understand.

  if (!quantityRange) {
    throw new Error(`No quantity range found for ${quantity} of ${variantID}.`);
  }

  const product = priceList.targets.find(
    (t) => t.variants.find((v) => v.id === variantID),
  );

  if (!product) {
    throw new Error(`Could not find a product related to variant ${variantID}.`);
  }

  const priceByProduct = priceList.prices[product.id][quantityRange.id].price;
  const priceByVariant = priceList.prices[product.id][quantityRange.id][variantID];

  return (priceByVariant || priceByProduct);
};

type QuantityLevels = {
  id: string;
  min: number;
  max: number;
}

interface NamedEntity {
  id: string;
  name: string; 
};

interface Product extends NamedEntity {
  variants: Array<NamedEntity>;
}

type PriceList = {
  targets: Array<Product>;
  quantityLevels: Array<QuantityLevels>;
  prices: {
    [productID: string]: {
      [quantityLevelID: string]: {
        price: number;
        [variantID: string]: number;
      }
    }
  }
};

type GetPriceProps = {
  variantID: string;
  quantity: number;
  priceList: PriceList;
};

>Solution :

Use a type assertion when instantiating the variable instead of an annotation. This will prevent TS from inferring that it is only false thereafter in the rest of the scope, even in code blocks which present difficulties regarding control flow analysis.

let quantityRange = false as QuantityLevels | false;

Your modified code in the TS Playground

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading