Typescript, Enums with strings and numbers - node.js

I have an interface with
interface mathTest {
mathAction: MathActionEnum;
}
The reason for this is that I want this property to have just one of the specific values from the enum below.
enum MathActionEnum {
'byOne' = 1,
'byTwo' = 2,
'byFour' = 4,
'byEight' = 8,
}
Assume mathAction = 'byOne' -> received from an API response.
First scenario: doing an arithmetic operation, I need the number value: let result: number = amount / MathActionEnum[mathAction] but I get an error:
The right-hand side of an arithmetic operation must be of type 'any',
'number', 'bigint' or an enum type
It is a number but still I need to cast it with Number(MathActionEnum[mathAction]) for the error to go away.
Second scenario: equality check, I need the string value: if (mathAction === MathActionEnum[MathActionEnum.byOne]) but I get an error:
This condition will always return 'false' since the types
'MathActionEnum' and 'string' have no overlap
Which makes sense.
I'm a bit lost, is there a way to syntax it as I expect it to be? Maybe I need to define things differently?
Thanks

TypeScript enums are absolutely NOT suitable for any sort of key-value mapping. The intent is to have a grouping of uniquely identifiable labels, but labels are where it ends. While they may indeed have a number representation under the hood, they are not intended for use as a key-value store. You will have to cast it to "extract the number", and then the type is just number, so you effectively defeat the purpose of enums.
For all intents and purposes, think of them like keys with no useful values:
const MathActionEnum = Object.freeze({
byOne: Symbol(),
byTwo: Symbol(),
byFour: Symbol(),
byEight: Symbol(),
})
Consider the newer alternative, const assertion, instead. They'll provide you with type safety on both keys and values:
const MathActions = {
'byOne': 1,
'byTwo': 2,
'byFour': 4,
'byEight': 8,
} as const
type MathAction = keyof typeof MathActions
type MathActionValue = typeof MathActions[MathAction]
You get full type safety on both keys and values:
const example = (action: MathAction) => {
return 2 * MathActions[action]
}
example('byOne')
// compile error, not a valid key
example('foo')
// -------------
const example2 = (actionValue: MathActionValue) => {
return 2 * actionValue
}
example2(4)
// compile error, not a valid value
example2(19)
You can even add type assertions to check if arbitrary values are a key or value:
const isAction = (action: string): action is MathAction => {
return Object.keys(MathActions).includes(action)
}
isAction
const isActionValue = (actionValue: number): actionValue is MathActionValue => {
return Object.values(MathActions).includes(actionValue as any)
}
You'll even get IDE autocompletion for both keys and values:
Here's a Playground

Related

Formik initial value partially undefined to result type

with a type icecream i have this model.
enum Sugar {
High = 1,
Medium = 2,
Low = 3,
}
Type IceCream = {
Name:string;
SugarContent: Sodium
}
now, in my formik form i wanna create a form with initial values, where sugarcontent is undefined. But it has to be set in the form according to my validiationschema (yup)
sugar: yup
.number()
.required("Sugar content is required")
.min(1)
.max(3)
is it possible to get a correctly typed form output for this? Something like the following as an idea
Type FormIceCream = {
Name:string;
SugarContent?: Sugar
}
const InitialValues:FormIceCream = {
Name:"",
SugarContent:undefined
}
return <Formik<IceCream>
initialValues={InitialValues} // <-- warning, Type 'undefined' is not assignable to type 'SugarContent'
validationSchema={IceCreamValidation}
onSubmit={(values:IceCream) => console.log(values)}
> ...

How to implement a type safe, phantom types based builder in typescript?

The idea is to allow a call to the .build() method only upon having all the mandatory parameters filled. So the constructor should be taught to do some validation.
If I understand you correctly, you have some kind of builder class, which by default doesn't have all the required parameters. And that class has a method, which updates its state. Only when all required parameters are set, only then build method should be available.
So first of all, we have T type which partial (all properties are optional).
On each update, we should return a new instance with type T & Record<K, T[K]> - it means optional T + one non-optional property.
Finally, we can use conditional types to allow build only when T extends Required<T>.
So the final solution:
function createBuilder<T>(initialData: T) {
return {
update: <K extends keyof T>(key: K, value: T[K]) => {
return createBuilder<T & Record<K, T[K]>>({
...initialData,
[key]: value
})
},
build: (() => {
//
}) as T extends Required<T> ? () => boolean : undefined
}
}
const builder1 = createBuilder<Partial<{
key1: string,
key2: number
}>>({})
builder1.build()
// Cannot invoke an object which is possibly 'undefined'
const builder2 = builder1.update('key1', 'abc')
builder2.build()
// Cannot invoke an object which is possibly 'undefined'
const builder3 = builder2.update('key2', 10)
builder3.build()
// No error
Hovewer, there is no point having this logic. If you can statically update the object, you probably can set all properties in the constructor.

Expect positive number parameter passed - jest

The test is linked to this question here which I raised (& was resolved) a few days ago. My current test is:
// Helpers
function getObjectStructure(runners) {
const backStake = runners.back.stake || expect.any(Number).toBeGreaterThan(0)
const layStake = runners.lay.stake || expect.any(Number).toBeGreaterThan(0)
return {
netProfits: {
back: expect.any(Number).toBeGreaterThan(0),
lay: expect.any(Number).toBeGreaterThan(0)
},
grossProfits: {
back: (runners.back.price - 1) * backStake,
lay: layStake
},
stakes: {
back: backStake,
lay: layStake
}
}
}
// Mock
const funcB = jest.fn(pairs => {
return pairs[0]
})
// Test
test('Should call `funcB` with correct object structure', () => {
const params = JSON.parse(fs.readFileSync(paramsPath, 'utf8'))
const { arb } = params
const result = funcA(75)
expect(result).toBeInstanceOf(Object)
expect(funcB).toHaveBeenCalledWith(
Array(3910).fill(
expect.objectContaining(
getObjectStructure(arb.runners)
)
)
)
})
The object structure of arb.runners is this:
{
"back": {
"stake": 123,
"price": 1.23
},
"lay": {
"stake": 456,
"price": 4.56
}
}
There are many different tests around this function mainly dependent upon the argument that is passed into funcA. For this example, it's 75. There's a different length of array that is passed to funcB dependent upon this parameter. However, it's now also dependent on whether the runners (back and/or lay) have existing stake properties for them. I have a beforeAll in each test which manipulates the arb in the file where I hold the params. Hence, that's why the input for the runners is different every time. An outline of what I'm trying to achieve is:
Measure the array passed into funcB is of correct length
Measure the objects within the array are of the correct structure:
2.1 If there are stakes with the runners, that's fine & the test is straight forward
2.2 If not stakes are with the runners, I need to test that; netProfits, grossProfits, & stakes properties all have positive Numbers
2.2 is the one I'm struggling with. If I try with my attempt below, the test fails with the following error:
TypeError: expect.any(...).toBeGreaterThan is not a function
As with previous question, the problem is that expect.any(Number).toBeGreaterThan(0) is incorrect because expect.any(...) is not an assertion and doesn't have matcher methods. The result of expect.any(...) is just a special value that is recognized by Jest equality matchers. It cannot be used in an expression like (runners.back.price - 1) * backStake.
If the intention is to extend equality matcher with custom behaviour, this is the case for custom matcher. Since spy matchers use built-in equality matcher anyway, spy arguments need to be asserted explicitly with custom matcher.
Otherwise additional restrictions should be asserted manually. It should be:
function getObjectStructure() {
return {
netProfits: {
back: expect.any(Number),
lay: expect.any(Number)
},
grossProfits: {
back: expect.any(Number),
lay: expect.any(Number)
},
stakes: {
back: expect.any(Number),
lay: expect.any(Number)
}
}
}
and
expect(result).toBeInstanceOf(Object)
expect(funcB).toHaveBeenCalledTimes(1);
expect(funcB).toHaveBeenCalledWith(
Array(3910).fill(
expect.objectContaining(
getObjectStructure()
)
)
)
const funcBArg = funcB.mock.calls[0][0];
const nonPositiveNetProfitsBack = funcBArg
.map(({ netProfits: { back } }, i) => [i, back])
.filter(([, val] => !(val > 0))
.map(([i, val] => `${netProfits:back:${i}:${val}`);
expect(nonPositiveNetProfitsBack).toEqual([]);
const nonPositiveNetProfitsLay = ...
Where !(val > 0) is necessary to detect NaN. Without custom matcher failed assertion won't result in meaningful message but an index and nonPositiveNetProfitsBack temporary variable name can give enough feedback to spot the problem. An array can be additionally remapped to contain meaningful values like a string and occupy less space in errors.

Inconsistent error while storing a darray into a Shape

I have a shape like this
const type TFileInfo = shape(
'displayName' => string,
'givenName' => string,
'jobTitle' => string,
'businessPhones' => vec<string>
);
private Person::TFileInfo $person;
Now my constructor of the class looks like so
public function __construct(string $apiresponse) { // instance method
$json = \json_decode($response, /* associative = */ true);
TypeAssert\matches<self::TFileInfo>($json);
$this->person = $json; //OFFENDING LINE
$this->person['businessPhones1'] = "";
}
Now strangely the above code does not throw any error .
If I remove the offending line , then the last line throws a compile time error Expected nothing because the field 'businessPhones1' is not defined in this shape type, and this shape type does not allow unknown fields
What am I missing here ? Is there a better way to assign an API response to a typed variable ?
TypeAssert\matches doesn't prove that its argument is the type you specified, in contrast to the behavior of some other built-ins like is_null which are special-cased in the typechecker. Instead, it coerces the argument and returns it, so you need to move your standalone call to the assignment, i.e. $this->person = TypeAssert\matches<self::TFileInfo>($json);.
You might have expected a type error from the $this->person = $json assignment then, but in fact json_decode and some other unsafe built-in PHP functions are special-cased by the typechecker to be bottom types (convertible to anything) so they could be usable at all before type-assert. It remains this way today: see its type definition in the HHVM source, probably for compatibility.
One other interesting point about this case is that $this->person = $json coerces $this->person to a bottom type as well downstream of the binding. To my understanding, this is a specific behavior of the Hack typechecker to do this for a single level of property nesting, yet it preserves the types for properties of properties (the second example has_error):
<?hh // strict
class Box<T> { public function __construct(public T $v) {} }
function no_error<T>(Box<int> $arg): T {
$arg->v = json_decode('');
return $arg->v;
}
function has_error<T>(Box<Box<int>> $arg): T {
$arg->v->v = json_decode('');
return $arg->v->v;
}

Typescript - Nested arrow function typing

I have this code for deferring the execution of a function
export type DeferredFunction<T> = () => T | PromiseLike<T>;
export class Deferrable<T> {
protected df: DeferredFunction<T>;
constructor(df: DeferredFunction<T>) {
this.df = df;
}
public async execute(): Promise<T> {
return this.df();
}
}
export const defer = <T>(df: DeferredFunction<T>): Deferrable<T> => new Deferrable<T>(df);
That works fine and I can run code like
await defer(() => someFunction('foo', 'bar')).execute();
but I what I want to do is type DeferredFunction in a way that I can specify the inner function's signature but I can't get it working. In generic cases the above works but when I want to limit the arguments such that they are specific to a certain type of function I don't have that kind of control.
For clarity, I want to be able to type the inner function's inputs like (as an example)
export type InnerDeferredFunction<T> = (a: string, b: number, c: SomeObjectType) => T | PromiseLike<T>
Any help would be greatly appreciated!
What "inner function" are you talking about? Is it someFunction? If so then the type of DeferredFunction<T> has no handle on it, since it's a function called by the implementation of DeferredFunction<T>. There's no way in TypeScript to specify "a function whose implementation must call a function of type (x: string, y: number, z: boolean) => string". Implementation details are not part of a function's call signature.
The only way I can imagine to begin to approach this would be for DeferredFunction<T> to accept as a parameter the inner function you want to call, along with the list of arguments to call it with. This might not be what you're looking for, but it's the closest that the type system can represent.
Something like this:
export type InnerDeferredFunction<T, A extends any[]> = (...args: A) => T | PromiseLike<T>;
export type ZeroArgDeferredFunction<T> = InnerDeferredFunction<T, []>
Here I'm keeping A generic but you can specify it to some hardcoded list of arguments. I've renamed your DeferredFunction to ZeroArgDeferredFunction to be explicit that it doesn't need arguments.
But now Deferrable needs to know about T and A:
export class Deferrable<T, A extends any[]> {
protected df: ZeroArgDeferredFunction<T>;
constructor(df: InnerDeferredFunction<T, A>, ...args: A) {
this.df = () => df(...args);
}
public async execute(): Promise<T> {
return this.df();
}
}
And you can see that you have to construct one by passing it the inner function and its arguments, and the ZeroArgDeferredFunction is built inside the constructor and is not passed in.
There are different ways to define defer(). It could be a thin wrapper around new Deferrable the way you had it, or you could imagine splitting it up so that the args come first:
export const defer = <A extends any[]>(...args: A) => <T>(
df: InnerDeferredFunction<T, A>): Deferrable<T, A> => new Deferrable<T, A>(df, ...args);
And then you can test it like this:
function someFunction(x: string, y: string) {
return (x + y).length;
}
function anotherFunction(x: number, y: number) {
return (x * y).toFixed()
}
const deferFooBar = defer('foo', 'bar');
await deferFooBar(someFunction).execute(); // okay
await deferFooBar(anotherFunction); // error! string is not assignable to number
Once you call deferFooBar('foo', 'bar'), the returned value will only accept functions that can be safely called with the arguments foo and 'bar'. That means someFunction will be accepted and anotherFunction will be rejected.
Okay, hope that helps; good luck!
Playground link to code

Resources