Reputation: 117
I'm now trying to type entity called Operators. It looks like this and as you can see has many commons. But there are some differences in values, for example callback function by count
key. Sometimes it expects 1 number, sometimes 2.
const operatorEntities: Record<OperatorsKeys, OperatorValue> = {
sin: {
textForInput: "sin",
onClick,
type: UNARY_OPERATOR_TYPE,
innerHtml: "sin",
count: Math.sin,
level: 3,
className: "sinus btn btn-outline-primary",
},
cos: {
textForInput: "cos",
onClick,
type: UNARY_OPERATOR_TYPE,
innerHtml: "cos",
level: 3,
count: Math.cos,
className: "sinus btn btn-outline-primary",
},
"!": {
textForInput: "!",
onClick,
type: UNARY_OPERATOR_TYPE,
innerHtml: "!n",
level: 3,
count: function factorialize(num): number {
if (num < 0) return -1;
else if (num == 0) return 1;
else {
return num * factorialize(num - 1);
}
},
className: "factorial btn btn-outline-primary",
},
"+": {
textForInput: "+",
onClick,
type: BINARY_OPERATOR_TYPE,
innerHtml: "+",
level: 1,
count: (left, right) => left + right,
className: " plus btn btn-outline-primary",
},
"-": {
textForInput: "-",
onClick,
type: BINARY_OPERATOR_TYPE,
innerHtml: "-",
level: 1,
count: (left, right) => left - right,
className: "minus btn btn-outline-primary",
}
}
So if I make type of the count function this way
type CountFunction = (operand1: number, operand2?: number) => number;
Then it will be the mistake in such a function
count: (a, b) => a + b;
Because b argument may be undefined. In this case I wanted to ask if there is any way to type function with 2 arguments and the second one should be optional? Or i just have to make 2 types for function with one and two arguments?
Upvotes: 1
Views: 269
Reputation: 328132
As you note, it's not safe to assign a function of type (o1: number, o2: number) => number
to a variable of type (o1: number, o2?: number) => number
, because such a function expects o2
to be a number
and might explode when unknowingly dereferencing an undefined
at runtime. According to the rules for function subtyping, (o1: number, o2: number) => number
is a supertype of (o1: number) => number
, so your CountFunction
would just be
type CountFunction = (o1: number, o2: number) => number;
That might be surprising, but the TypeScript compiler takes the view that a function of the form (o1: number) => number
will probably just ignore a second argument, and so it's harmless to allow the assignment. So you can't make o2
optional, but you can omit it altogether. Such confusion between optional parameters and shortened parameter lists is common enough that it is explicitly discussed in the TypeScript Handbook.
So that's the answer to your question as asked, but it's not what you want, is it? It will let your assignments succeed:
enum OperatorType {
UNARY,
BINARY
}
type OperatorThing = {
type: OperatorType,
count: (x: number, y: number) => number
}
const operatorEntities: Record<string, OperatorThing> = {
sin: { type: OperatorType.UNARY, count: Math.sin, },
cos: { type: OperatorType.UNARY, count: Math.cos, },
"!": {
type: OperatorType.UNARY, count: function factorialize(num): number {
if (num < 0) return -1;
else if (num == 0) return 1;
else {
return num * factorialize(num - 1);
}
},
},
"+": { type: OperatorType.BINARY, count: (left, right) => left + right, },
"-": { type: OperatorType.BINARY, count: (left, right) => left - right, }
}
but when you try to actually call those count
methods you'll find that the compiler now requires you to pass in a second argument no matter what, even if you check the type
property:
Object.keys(operatorEntities).forEach(k => {
const op = operatorEntities[k];
if (op.type === OperatorType.UNARY) {
op.count(1); // error!
// ~~~~~~~~ <-- expected 2 arguments but got 1
} else {
op.count(2, 3);
}
})
So it isn't very helpful to have a function type that represents the union of the possible function types you have. You want a way to discriminate among them.
My suggestion here is to keep your separate one- and two-argument function types, and define OperatorThing
as a discriminated union where the type
property acts as a discriminant that can be used to determine which function type you have:
type OperatorThing =
{ type: OperatorType.UNARY, count: (o1: number) => number } |
{ type: OperatorType.BINARY, count: (o1: number, o2: number) => number }
The assignment of operatorEntities
still succeeds, but now you can call functions with the right number of arguments:
Object.keys(operatorEntities).forEach(k => {
const op = operatorEntities[k];
if (op.type === OperatorType.UNARY) {
op.count(1); // okay
} else {
op.count(2, 3); // okay
}
})
Upvotes: 1