Using TypeScript 5.0.2
I have a function that returns an array of 3 functions, I want the purchase function to be typed correctly so that it only requires a requirement
param if the product specified one (identified in the products
type object). This is the base code that I'm working with, currently both the purchaseSoda()
and the purchaseBeer()
methods require a requirements
param however there is no requirements on soda so I would only expect the purchaseBeer()
method to throw an error as there is no requirements object passed to the method.
I don't want to have to call a method that doesn't require a param like this purchaseSoda(undefined);
type InventoryItem<Product, Restrictions = undefined> = {
product: Product,
restrictions: Restrictions
}
type Beverage = { sizeInOunces: number, price: number };
type Book = { title: string, price: number }
type AgeRequirement = { age: number }
type products = {
"soda": InventoryItem<Beverage>,
"beer": InventoryItem<Beverage, AgeRequirement>,
"comic": InventoryItem<Book>,
"Adult Magazine": InventoryItem<Book, AgeRequirement>
};
const getItem = <
productName extends keyof products,
item extends products[productName],
product extends item["product"],
restrictions extends item["restrictions"]
>
(name: productName): [
(item: product) => void,
(restrictions: restrictions) => product,
() => void
] => {
const addItemToInventory = (item: product) => { throw new Error("omitted for stackoverflow"); }
const cancelPurchase = () => { throw new Error("omitted for stackoverflow"); }
const purchase = (restrictions: restrictions): product => { throw new Error("omitted for stackoverflow"); }
return [addItemToInventory, purchase, cancelPurchase];
}
const [addSoda, purchaseSoda, cancelPurchaseSoda] = getItem("soda");
const [addBeer, purchaseBeer, cancelPurchaseBeer] = getItem("beer");
purchaseSoda(); // Shouldn't Require a Param
purchaseBeer(); // Should Error here, because the beer product has a age requirement.
purchaseBeer({ age: 21 }); // Should require a param
I've tried using never
, null
on the default assignment for the InventoryItem
I've converted the getItem
and purchase
method to a function with an override.
I've modified the return signature of getItem
so that its
(name: productName): [
(item: product) => void,
(() => product) |
((restrictions: restrictions) => product),
() => void
] => {
I've changed the signatures of the purchase
method to make the param optional (using '?')
(name: productName): [
(item: product) => void,
(restrictions?: restrictions) => product,
() => void
] => {
const addItemToInventory = (item: product) => { throw new Error("omitted for stackoverflow"); }
const cancelPurchase = () => { throw new Error("omitted for stackoverflow"); }
const purchase = (restrictions?: restrictions): product => { throw new Error("omitted for stackoverflow"); }
While this now allows me to call purchaseSoda()
with out a param, I can now also call purchaseBeer()
with out one as well, and purchaseBeer()
has a age requirement that needs to be passed in. So just adding the '?' doesn't solve my problem.
I've also read through a bunch of docs.
Playing around with this for a few hours now, and I feel stuck.
Any help on this would be much appreciated, thanks for your time.
TypeScript doesn't automatically assume that a function parameter that accepts undefined
can be treated as an optional parameter, so if you want such behavior you'll need to do some type manipulations that express that.
Your function type is equivalent to something like
(restrictions: R) => P
which will always require a restrictions
argument even if undefined
is assignable to R
.
What you'd like instead is a function type where if undefined
is in the domain of R
then it's (restrictions?: R) => P
(note the ?
), and otherwise it's (restrictions: R) => P
. There are multiple ways to write this, but here's one using a conditional type for the whole function type:
type UndefIsOptional<R, P> =
undefined extends R ?
(restrictions?: R) => P :
(restrictions: R) => P;
Now we can use this function type instead:
const getItem = <
K extends keyof Products,
I extends Products[K],
P extends I["product"],
R extends I["restrictions"]
>
(name: K): [
(item: P) => void,
UndefIsOptional<R, P>, // this used to be (r: R) => P
() => void
] => {
const addItemToInventory = (item: P) => { throw new Error("omitted"); }
const cancelPurchase = () => { throw new Error("omitted"); }
// this also used to be (r: R) => P
const purchase: UndefIsOptional<R, P> = (r?: R): P =>
{ throw new Error("omitted"); }
return [addItemToInventory, purchase, cancelPurchase];
}
And now we can test it:
const [addSoda, purchaseSoda, cancelPurchaseSoda] = getItem("soda");
purchaseSoda(); // okay
purchaseSoda({ age: 21 }); // error
const [addBeer, purchaseBeer, cancelPurchaseBeer] = getItem("beer");
purchaseBeer(); // error
purchaseBeer({ age: 21 }); // okay
Looks good. And let's just add another type of thing where the requirement is itself optional:
⋯
"optionalId": InventoryItem<Book, AgeRequirement | undefined>;
⋯
And you'll see that both ways of calling it are acceptable:
const [, purchaseOpt,] = getItem("optionalId");
purchaseOpt(); // okay
purchaseOpt({ age: 21 }); // okay