Formik initial value partially undefined to result type - typescript-typings

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)}
> ...

Related

Typescript, Enums with strings and numbers

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

interfaces in typescript: use function parameter on a nested object reference

I have this object model:
export interface UpdateDocument {
updated_at?: string;
actions?: Actions;
}
export interface Actions {
update?: Update;
create?: Create;
}
export interface Update {
name?: Values;
priority?: Values;
engine?: Values;
fact?: Fact;
}
export interface Fact {
brand?: Values;
model?: Values;
version?: Values;
year?: Values;
km?: Values;
color?: Values;
}
export interface Values {
old?: any;
new?: any;
}
export interface Create {
conditions?: Object;
recipe?: Object;
}
In this function i tried to pass a parameter to references an objects field and do an assignment:
async buildUpdateDocument (updateDocument: UpdateDocument) {
let fields: Array<string> = ['name','priority','engine','fact','adjustment'];
fields.forEach((field: string) =>{
updateDocument.actions!.update![field]!.old = await this.getValue(field)
})
}
but i hav this ts-error: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Update'.
No index signature with a parameter of type 'string' was found on type 'Update'.ts(7053)
How can i pass the parameter in this kind of reference to do the assignment?
First of you have specified a wrong key adjustment that doesn't exist on Update. This example uses a explicit type (as const):
let fields = ['name','priority','engine','fact'] as const;
Make sure to not add a type definition to the variable when using as const.
Here is the modified function to better fit TS standards. This also addresses the forEach-async problem in the original code. The real correct structure would be null checks for each of the x | undefined types, but to override the type errors the following is the way to go.
async function buildUpdateDocument (updateDocument: UpdateDocument) {
const fields: Array<keyof Update> = ['name','priority','engine','fact'];
await Promise.all(fields.map(async (field) => {
(((updateDocument.actions as {update: Update}).update)[field] as Values).old = await this.getValue(field);
}));
}
Your current code has bugs that the type system would help you find if you let it. First, the adjustment field doesn't exist on the Update type, and old field doesn't exist on the Fact type.
To implement this properly, I would use a Record for the data type instead:
const updateFields = ['name', 'priority', 'engine', 'fact'] as const
export type UpdateFields = typeof updateFields[number]
export type Update = Record<UpdateFields, Values>
And then, your function will look like this:
async buildUpdateDocument (updateDocument: UpdateDocument) {
updateFields.forEach((field) =>{
updateDocument.actions!.update![field]!.old = await this.getValue(field)
})
}

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.

Forbid specific enum value for DTO in Nestjs

My "AppState" enum has following possible enum values:
export enum AppState {
SUCCESS,
ERROR,
RUNNING
}
I have a UpdateAppStateDTO with an appState which should accept every enum value except RUNNING.
export class UpdateAppStateDTO {
#IsEnum(AppState)
#NotEquals(AppState.RUNNING) // Doesn't work properly
public appState: AppState;
}
For the route I have this example
#Patch()
public setState(#Body() { appState }: UpdateAppStateDTO): void {
console.log(appState);
}
If the request has an empty body or a non valid enum value like "foobar" for appState I'm getting a 400, which is fine.
The problem is that when I send "RUNNING" I'm still getting a 200 instead of a 400.
How can I prevent this behaviour?
I assume you are sending in the string 'RUNNING', and you're trying to make sure that that is what is not used, correct? With what you've currently got, your enum maps to these values:
export enum AppState {
SUCCESS = 0,
ERROR = 1,
RUNNING = 2
}
So if you send in the string 'RUNNING', the validator checks that RUNNING !== 2 which is in fact true leading to successful validation. The #IsEnum() decorator checks that the value sent in in a valid key of the enum, so sending in 'RUNNING' passes that check, hence why you don't get some sort of error there.
The most verbose way to fix this is to make your enum a string enum like so:
export enum AppState {
SUCCESS = 'SUCCESS',
ERROR = 'ERROR',
RUNNING = 'RUNNING'
}
This will make each AppState value map to its corresponding string, though that does lead to having to type out a lot of declarations and can lead to duplicate code.
Another way to manage this is to set your #NotEquals() enum to the key provided by the enum value like so:
export class UpdateAppStateDTO {
#IsEnum(AppState)
#NotEquals(AppState[AppState.RUNNING])
public appState: AppState;
}
But keep in mind that with this approach when you look at appState later it will still be a numeric value instead of a string.
You can play around with this stackblitz I made for this to see some running code.
The problem is that when I send "RUNNING" I'm still getting a 200 instead of a 400.
it seems that you are using the string(!) "RUNNING" as value in your request payload as such:
{ appState: "RUNNING" }
In this case, IsEnum and NotEquals both regard the payload as valid.
Why is that?
First of all numeric enums are reverse mapped by typescript so your enum is internally (as javascript object) represented as follows:
{
'0': 'SUCCESS',
'1': 'ERROR',
'2': 'RUNNING',
'SUCCESS': 0,
'ERROR': 1,
'RUNNING': 2
}
Now class-validator's isEnum() is coded as follows:
isEnum(value: unknown, entity: any): boolean {
const enumValues = Object.keys(entity)
.map(k => entity[k]);
return enumValues.indexOf(value) >= 0;
}
and since the enum is reverse-mapped isEnum('RUNNNING', AppState) will return true.
At the same time NotEquals, which is coded as such...
notEquals(value: unknown, comparison: unknown): boolean {
return value !== comparison;
}
will compare the string 'RUNNING' against AppState.RUNNING (which equates to 2) and also conclude that this is valid since 'RUNNING' != 2.
So there you have it why the payload { appState: "RUNNING" } will result in a 200 instead of a 400 status code.
How can I prevent this behaviour?
The enum value AppState.RUNNING equates to 2 so when you make a request, you should use the numeric value of 2 in your payload:
{ appState: 2 }
In the above case, the class-validator's NotEquals validator will then correctly deny the request with the response containing:
"constraints": {
"notEquals": "appState should not be equal to 2"
}

How to use 'Decimal' as a field type in GraphQL schema?

I want to use Decimal as a type for latitude & longitude field in my GraphGL schema. But GraphQL provides only Float and Int.
Is there is any package to solve this?
I looked at the source code for this module can came up with this:
/// I M P O R T
import Big from "big.js";
import { GraphQLScalarType, Kind } from "graphql";
/// E X P O R T
export default new GraphQLScalarType({
name: "Decimal",
description: "The `Decimal` scalar type to represent currency values",
serialize(value) {
return new Big(value);
},
parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
// #ts-ignore | TS2339
throw new TypeError(`${String(ast.value)} is not a valid decimal value.`);
}
return Big(ast.value);
},
parseValue(value) {
return Big(value);
}
});
Then, I add the reference to my resolver so I can just add Decimal to my SDL code.

Resources