React Typescript useLocalStorage Hook - javascript

i wrote a custom hook to interact with the sessionStorage in React. Currently I don't like that I can just arbitrarily write any key-value pair in there. For testing and debugging purposes I would like to introduce a form of TypeSafety and I was thinking about maybe using a Union Type rather then the Generic.
I basically want to achieve two goals.
check if the key is a valid key that is allowed to be put into the sessionStorage
if the key is allowed make sure the type of the value is correct.
Does anyone have any idea how you would go about implementing these sort of checks in Typescript. Any help is appreciated.
export function useSessionStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.sessionStorage.getItem(key);// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}

In case someone stumbles across this question, I ended up finding how this is implemented in Typescript. You can achieve the desired effect by the use of the typeof operator on the generic.
basically you first define a interface or type:
type Allowed = {
item1: string
item2: boolean
item3: number
}
Then you can modify the function using the keyof operator
function useSessionStorage<T extends keyof Allowed>(key: T, initialValue: Allowed[T]){
{...}
}
the use of key of is better described here: https://www.typescriptlang.org/docs/handbook/advanced-types.html.
Hope this helps other people that might be struggling with this.

Related

Linter complaining can't use the spread operator on string, which is good, but how do I avoid it in this situation?

When I hover over ...res in VSCode I get the warning from my linter:
Spread types may only be created from object types
When I log res it's either a string or an object. However, I have no idea how to satisfy the linter in this case.
function getCookie(req: Request, key: string): string | undefined {
const {
headers: { cookie },
} = req;
return (
cookie &&
cookie.split(";").reduce<string | undefined>((res, item) => {
const data = item.trim().split("=");
return <string | undefined>{ ...res, [data[0]]: data[1] };
}, "")
);
}
So let's deal with the JavaScript and TypeScript aspects separately. In plain JavaScript terms, your function doesn't do what you've described. When there are cookies defined, it returns a Record<string, string> mapping cookie names to cookie values, not a particular string or false. If you mean for it to do something with key, you haven't included that part in your example.
When no cookies are defined, it ought to return undefined, but because you test the truthiness cookies in your return statement, it could return an empty string instead. That can be fixed by making that test separately and returning undefined explicitly. (Make sure your linter is warning you about using non-booleans in boolean context in TS.)
The reason you're spreading a string is that you're passing an empty string "" as the initial value to Array.prototype.reduce. Since you are accumulating objects, not strings, your initial value should be {} (an empty object) instead.
That helps us see the TypeScript issues. Your string | undefined casts are all unnecessary once the initial value is corrected, and the return type of the function overall becomes Record<string, string> | undefined. In sum:
function getCookie(req: Request, key: string): Record<string, string> | undefined {
const {
headers: { cookie },
} = req;
if (!cookie) {
return undefined;
}
return cookie.split(";").reduce<Record<string, string>>((res, item) => {
const data = item.trim().split("=");
return { ...res, [data[0]]: data[1] };
}, {});
}
However, if you actually want to look for a specific key and return its value, an array reducer isn't the best approach and your function would look rather different.

How to de-structure an enum values in typescript?

I have an enum in typescript like below:
export enum XMPPElementName {
state = "state",
presence = "presence",
iq = "iq",
unreadCount = "uc",
otherUserUnreadCount = "ouc",
sequenceID = "si",
lastSequenceID = "lsi",
timeStamp = "t",
body = "body",
message = "message"
}
And wants to de-structure its value, How can we do this in Typescript?
const { uc, ouc, msg, lsi, si, t, body } = XMPPElementName;
update
As #amadan mentioned, we can use Assigning to new variable names as in Mozilla doc say Destructuring_assignment, like below:
Assigning to new variable names
A property can be unpacked from an object and assigned to a variable with a different name than the object property.
const o = {p: 42, q: true};
const {p: foo, q: bar} = o;
console.log(foo); // 42
console.log(bar); // true
And the method is very good to solve this problem, but if you need to access all items without the need to explicitly define them, you can either on of these two mentiond tag1 tag2
const { uc, ouc, msg, lsi, si, t, body } = XMPPElementName;
This doesn't work because XMPPElementName doesn't have an element named uc (and equivalently for others). If you explicitly name your keys, it will work:
const {
unreadCount: uc,
otherUserUnreadCount: ouc,
message: msg,
lastSequenceID: lsi,
sequenceID: si,
timeStamp: t,
body: body,
} = XMPPElementName;
it will work. Alternately, you can just use variables with names that are equal to the keys, not the values:
const {
unreadCount,
otherUserUnreadCount,
message,
lastSequenceID,
sequenceID,
timeStamp,
body,
} = XMPPElementName;
You want an enum value-to-value map. Like you've said enum in JS is just a POJO. You can create a utility type to help generate the correct type.
type EnumValueMap<T extends { [k: string]: string }> = { [K in T[keyof T]]: K }
function convertEnumValuesToObject<T extends { [k: string]: string }>(enumerable: T): EnumValueMap<T> {
return (Object as any).fromEntries(Object.values(enumerable).map(v => [v, v]))
}
Playground Link
As we know, in typescript an enum is like a plain old javascript object(at-least what the playground js-output is showing or the log showing):
one way is using a function which generates a new object with {value:value} structure like below:
export function convertEnumValuesToObject<T>(enumObj: T): { [index: string]: T[keyof T] } {
const enum_values = Object.values(enumObj);
return Object.assign({}, ...enum_values.map(_ => ({ [_]: _ })));
}
const { uc, ouc, msg, lsi, si, t, body } = convertEnumValuesToObject(
XMPPElementName
);
It would be great to see answers in typescript?
This may be helpful for anyone looking for a quick and easy answer - yes you can (at least as of now). This works for enums with and without assigned values as far as I can tell.
enum MyEnum {
One,
Two,
Three
}
const { One, Two, Three } = myEnum;
console.log({ One, Two, Three }) // {One: 0, Two: 1, Three: 2}
enum Status {
None = '',
Created = 'CREATED',
Completed = 'COMPLETED',
Failed = 'FAILED',
}
const { None, Created, Completed, Failed } = Status;
console.log(None, Created, Completed, Failed) // '', 'CREATED', 'COMPLETED, 'FAILED'
Please write me back if I'm wrong or you found any weirdness when testing yourself.

TypeScript - Send optional field on POST only when there is value

I am using Formik, in my React application. I have a simple form with 3 fields. I am doing 2 operations with that form. Add/Edit Resources.
My Problem is that one field is optional. Meaning I should never send it, if its value is null. Currently, I send an empty string which is wrong.
I am using TS-React-Formik, and here is my code for the handleSubmit method:
interface IValues extends FormikValues {
name: string;
owner?: string;
groups: string[];
}
interface CreateAndEditProps {
doSubmit(service: object, values: object): AxiosResponse<string>;
onSave(values: IValues): void;
}
handleSubmit = (values: FormikValues, formikActions:FormikActions<IValues>) => {
const { doSubmit, onSave, isEditMode } = this.props;
const { setSubmitting } = formikActions;
const payload: IValues = {
name: values.name,
groups: values.groups,
owner: values.owner
};
const submitAction = isEditMode ? update : create;
return doSubmit(submitAction, payload)
.then(() => {
setSubmitting(false);
onSave(payload);
})
.catch(() => {
setSubmitting(false);
});
};
I thought a simple if statement would work, and while it does, I do not like it at all. Let me give you an example of why. If I add 2 more optional fields, as I am about to do, in a similar form, I do not want to do several if statements to achieve that.
If you could think of a more elegant and DRY way of doing it, It would be amazing. Thank you for your time.
Look at the removeEmptyKeys() below.
It takes in an Object and removes the keys that have empty string.It mutates the original Object, please change it accordingly if you expect a diff behaviour.
In your code after defining payload, I would simply call this method , removeEmptyKeys(payload)
Also it will resolve your if else problem.
removeEmptyKeys = (item)=>{
Object.keys(item).map((key)=>{
if(payload[key]===""){
delete payload[key]}
})}
var payload = {
one : "one",
two : "",
three : "three"
}
removeEmptyKeys(payload)
Please mark it as resolved if you find this useful.
For your code :
const removeEmptyKeys = (values: IValues): any => {
Object.keys(values).map((key) => {
if (payload && payload[key] === "")
{ delete payload[key] } })
return values;
}

Map Typescript Enum

How would I map a typescript enum? For example, with strings you can do this:
let arr = [ 'Hello', 'Goodbye' ];
arr.map(v => {
if (v === 'Hello') {
return ':)';
} else if (v === 'Goodbye') {
return ':(';
}
); // [ ':)', ':(' ]
This, of course, doesn't work with enums:
enum MyEnum { Hello, Goodbye };
MyEnum.map(v => {
if (v === MyEnum.Hello) {
return ':)';
} else if (v === MyEnum.Goodbye) {
return ':(';
}
}); // does not work
Ideally, I'd like to do this in a generalized way so I can simply take any enum I have and put it through a map function while preserving type information. Usage might look something like this:
map(MyEnum, v => {
if (v === MyEnum.Hello) {
return ':)';
} else if (v === MyEnum.Goodbye) {
return ':(';
}
}); // [ ':)', ':(' ]
I've been fiddling around with getting a function that does this for me but keep having issues getting the generics just right.
To map an enum do this:
(Object.keys(MyEnum) as Array<keyof typeof MyEnum>).map((key) => {})
The function to solve this is quite simple.
// you can't use "enum" as a type, so use this.
type EnumType = { [s: number]: string };
function mapEnum (enumerable: EnumType, fn: Function): any[] {
// get all the members of the enum
let enumMembers: any[] = Object.keys(enumerable).map(key => enumerable[key]);
// we are only interested in the numeric identifiers as these represent the values
let enumValues: number[] = enumMembers.filter(v => typeof v === "number");
// now map through the enum values
return enumValues.map(m => fn(m));
}
As you can see, we first need to get all of the keys for the enum (MyEnum.Hello is actually 1 at runtime) and then just map through those, passing the function on.
Using it is also simple (identical to your example, although I changed the name):
enum MyEnum { Hello, Goodbye };
let results = mapEnum(MyEnum, v => {
if (v === MyEnum.Hello) {
return ':)';
} else if (v === MyEnum.Goodbye) {
return ':(';
}
});
console.log(results); // [ ':)', ':(' ]
The reason we need to filter the enum to be numbers only is because of the way enums are compiled.
Your enum is actually compiled to this:
var MyEnum;
(function (MyEnum) {
MyEnum[MyEnum["Hello"] = 0] = "Hello";
MyEnum[MyEnum["Goodbye"] = 1] = "Goodbye";
})(MyEnum || (MyEnum = {}));
;
However we are not interested in "Hello" or "Goodbye" as we can't use those at runtime.
You will also notice a funny type statement right before the function. This is because you can't type a parameter as someParameter: enum, you need to explicitly state it as a number -> string map.
Mapping in Typescript can be extremely powerful for writing less code.
I have been using key value Enum mapping a lot recently and would recommend it!
Here are a couple of examples!
Basic enum usage
enum InlineStyle {
"Bold",
"Italic",
"Underline"
}
type IS = keyof typeof InlineStyle
// Example of looping
(Object.keys(InlineStyle) as Array<IS>).forEach((key) => {
// code here
})
// Example of calling a function
const styleInline = (style: IS) => {
// code here
}
Enum key value usage
enum ListStyle {
"UL" = "List",
"OL" = "Bullet points"
}
// Example of looping
Object.entries(ListStyle).forEach(([key, value]) => {
// code here
})
Interface mapping
enum InlineStyle {
"Bold" = "isBold",
"Italic" = "isItalic",
"Underline" = "isUnderlined"
}
type InlineStyleType = Record<InlineStyle, boolean>
enum ListStyle {
"UL",
"OL"
}
type LS keyof typeof ListStyle
interface HTMLBlock extends InlineStyleType {
// This has extended with
// isBold: boolean
// isItalic: boolean
// isUnderlined: boolean
listType: LS
}
With ts-enum-util (npm, github), it's easy, type-safe (uses generics), and takes care of skipping the numeric reverse lookup entries for you:
import { $enum } from "ts-enum-util";
enum MyEnum { Hello, Goodbye };
$enum(MyEnum).map(v => {
if (v === MyEnum.Hello) {
return ':)';
} else if (v === MyEnum.Goodbye) {
return ':(';
}
}); // produces [':(', ':)']
NOTE: ts-enum-util always iterates based on the order of the sorted enum keys to guarantee consistent order in all environments. Object.keys() does not have a guaranteed order, so it's impossible to iterate enums "in the order they were defined" in a cross-platform guaranteed way.
(update: new version of ts-enum-util now preserves the original order in which the enum was defined)
If you are using string enums, then combine it with ts-string-visitor (npm, github) for even more generic type-safe compiler checks to guarantee that you handle all possible enum values in your map function:
(update: new version of ts-enum-util now includes functionality of ts-string-visitor, and it works on numeric enums now too!)
import { $enum } from "ts-enum-util";
import { mapString } from "ts-string-visitor";
enum MyEnum { Hello = "HELLO", Goodbye = "GOODBYE" };
$enum(MyEnum).map(v => {
// compiler error if you forget to handle a value, or if you
// refactor the enum to have different values, etc.
return mapString(v).with({
[MyEnum.Hello]: ':)',
[MyEnum.Goodby]: ':('
});
}); // produces [':(', ':)']
I would not call it general but I use this many times and may it will be handy for others too:
type TMyEnum = ':)'|':(';
class MyEnum {
static Hello: TMyEnum = ':)';
static Goodbye: TMyEnum = ':(';
}
console.log(MyEnum.Hello); // :)
console.log(MyEnum.Goodbye); // :(
Now you don't need any mapping function and it works as expected however you have to create separate similar class for every enum (which should not be a problem since you would do at anyway). The only drawback I can think now is that you can not iterate over it's properties. But until now it wasn't a problem for me I didn't need it. And you can add a static array to the class when you need it.
Maybe this will help you:
enum NumericEnums {
'PARAM1' = 1,
'PARAM2',
'PARAM3',
}
enum HeterogeneousEnums {
PARAM1 = 'First',
PARAM2 = 'Second',
PARAM3 = 3,
}
type EnumType = { [key: string]: string | number };
type EnumAsArrayType = {
key: string;
value: string | number;
}[];
const enumToArray = (data: EnumType): EnumAsArrayType =>
Object.keys(data)
.filter((key) => Number.isNaN(+key))
.map((key: string) => ({
key,
value: data[key],
}));
console.log(enumToArray(NumericEnums));
console.log(enumToArray(HeterogeneousEnums));
// Usage
enumToArray(HeterogeneousEnums).map(({ key, value }) => {
console.log(`${key}: ${value}`);
// Your necessary logic
return null;
});
Console result
This is a working function you can use. Below I'm passing ItemMaterial to getEnumKeys function and getting ["YELLOW", "WHITE", "ROSE", "BLACK"].
Similarly use the getEnumValues function to get values of the enum.
Take a look at the splitEnumKeysAndValues function to see how these variables extracted from the enum.
enum ItemMaterial {
YELLOW,
WHITE,
ROSE,
BLACK,
}
const keys = getEnumKeys<typeof ItemMaterial>(ItemMaterial)
const values = getEnumValues<typeof ItemMaterial, `${ItemMaterial}`>(ItemMaterial);
function getEnumKeys<TypeofEnum>(value: TypeofEnum): keyof TypeofEnum {
const { values, keys } = splitEnumKeysAndValues(value);
return keys as unknown as keyof TypeofEnum;
}
function getEnumValues<TypeofEnum, PossibleValues>(value: TypeofEnum): PossibleValues[] {
const { values, keys } = splitEnumKeysAndValues(value);
return values as unknown as PossibleValues[];
}
function splitEnumKeysAndValues<T>(value: T): { keys: keyof T, values: Array<string | number> } {
const enumKeys = Object.keys(value);
const indexToSplit = enumKeys.length / 2
const enumKeysKeyNames = enumKeys.slice(0, indexToSplit) as unknown as keyof T;
const enumKeysKeyValues = enumKeys.slice(indexToSplit);
return {
keys: enumKeysKeyNames,
values: enumKeysKeyValues,
}
}

Flow types on objects

I've been looking at adding Flow to my javascript project.
In several cases, I do something like this, I have an object.
const myObject = {
x: 12,
y: "Hello World"
}
And I have a general function that does some mapping on the object, keeping the keys but replacing the values.
function enfunctionate(someObject) {
return _.mapValues(myObject, (value) => () => value)
}
I'd like the function to return the type {x: () => number, y: () => string} Is there a way to make this happen?
Of course, I could type it more generically as {[key: string]: any} but I'd lose a lot of the static typing I'd like to gain by using flow.
If I could replace my few cases that do this with code generation or macros that could work, but I don't see a good way to do that with flow.
Is there a way to solve this problem?
This is the best you can get at the moment, while it's safer than {[key: string]: any} there's still an any involved
function enfunctionate<O: Object>(someObject: O): { [key: $Keys<O>]: (...rest: Array<void>) => any } {
return _.mapValues(someObject, (value) => () => value)
}
const obj = enfunctionate(myObject)
obj.x() // ok
obj.unknown() // error
EDIT: starting from v.33 you can use $ObjMap
function enfunctionate<O>(o: O): $ObjMap<O, <V>(v : V) => () => V> {
return _.mapValues(o, (value) => () => value)
}
Flow is able to infer a lot of things. However, if you want to pin the types, then you have to create the types yourself. You need to create a type for the object you are passing to enfunctionate and then specify the type that is returned.
You can type the function with type alias. So, you can create a new explicit type for that function.
function enfunctionate<X, Y>(x: X): Y {
return _.mapValues(myObject, (value) => () => value)
}
As long as you know the type of X, then you know the type of Y. Type aliasing, generics, and unions should be able to give you flexibility you need.
However, I'm also thinking as long as the type of mapValues is specified well (hopefully by someone else) then you can get away with just specifying the type that enfunctionate takes and let Flow infer the return type of that function.

Categories

Resources