how to turn an observable object into an observable array - javascript

I have this observable object in my angular project that has this type:
export interface FavoritesResponse {
wallet: boolean;
deposit: boolean;
withdraw: boolean;
transfer: boolean;
exchange: boolean;
ticket: boolean;
account: boolean;
}
I want to extract an array from this object with only the properties that have the value true.
So for example if my favorites object looks like this:
favorites$ = {
wallet: true;
deposit: true;
withdraw: false;
transfer: false;
exchange: false;
ticket: true;
account: true;
}
I want to have my enabledFavorites$ look like this:
enabledFavorites$ = [
wallet,
deposit,
ticket,
account
]
as in, turn it into an array and only have the keys that had the value of true. How can I do this? I know the solution probably contains an rxjs pipe, map but I don't know what I should be doing exactly.

If you mean to say the observable emits an object of type FavoritesResponse and you wish to transform the emission to an array of it's keys only with value true, you could use
RxJS map operator to transform the incoming object
Native JS methods Object.keys() with Array#filter to perform the actual transformation
enabledFavorites$: Observable<string[]> = favorites$.pipe(
map((favs: FavoritesResponse) =>
Object.keys(favs).filter((key: string) => !!favs[key])
)
);

get favoritesArray$(): Observable<string[]> {
return this.favoritesSettings$.pipe(
map((favorites) => {
const _items = Object.keys(favorites);
const _result: string[] = [];
_items.forEach((item) => {
if (!!(favorites as any)[item]) {
_result.push(item);
}
});
return _result;
})
);
}

So I guess that you want is convert from Observable<FavoritesResponse> to Observable<string[]> with the string[] containing the keys checked.
One way could be:
const enabledFav$ = favOptions$.pipe(
map(resp => {
let result = [];
Object.keys(resp).forEach(key => {
if (resp.key) {
result.push(key);
}
});
return result;
})
);
I dont know the scenario but this is basically the code.
Here enabledFav$is Observable<string[]> and favOptions$ is Observable<FavoritesResponse>

Related

How to extend map operator in rxjs?

I want to create a new operator like map. but the different is whatever map returns I need to clone it using JSON.parse(JSON.stringify(value)).
Because I return an object from my store (Subject), and I don't want somebody to change it, or it's inner properties.
I copy the map singture to my mapToVM function, but I can't find any way to use pipe or manipulate the return results.
Is it possible to change the return results by wrapping the map operator?
codesandbox
So I try to return the map function and take the source as Observable, but typescript
import { map, Observable, OperatorFunction, Subject } from "rxjs";
console.clear();
const store = new Subject<{ company: { id: number } }>();
store.pipe(map((s) => s.company)).subscribe((company) => {
console.log({ company });
});
function mapToVM<T, R>(
project: (value: T, index: number) => R
): OperatorFunction<T, R> {
// JSON.parse(JSON.stringify(source));
return map(project);
}
store.pipe(mapToVM((s) => s.company)).subscribe((company) => {
console.log({ company });
});
store.next({ company: { id: 1 } });
you can use the static pipe function:
function jsonClone<T>(): OperatorFunction<T, T> {
return pipe(map((source) => JSON.parse(JSON.stringify(source))));
}
Full CodeSandbox example

Is it possible to make a localStorage wrapper with TypeScript typed parameters and mapped return values?

I'm trying to construct a TS wrapper for localStorage with a TS schema that defines all the possible values in localStorage. Problem is that I can't figure out how to type the return value so that it's mapped to the approriate type from the schema: for example if I want to call LocalStorage.get("some_number"), I want the return type to be of type number. Is this even possible in TS?
Keys -typed parameter value works really well for the input!
type Values = LocalStorageSchema[Keys] returns the union type of the values which is not what I'm looking for.
Also it seems that it's not possible to use the variable key in the get function for type narrowing...
I have also looked into generic types: LocalStorage.get(...) but I think that kind of defeats the whole point of typing the return value.
Anyone got ideas for this? Thanks!
type LocalStorageSchema = {
token: string;
some_string: string;
some_number: number;
};
type Keys = keyof LocalStorageSchema;
// type Values = LocalStorageSchema[Keys]; ???
export const LocalStorage = {
get(key: Keys): any {
const data = window.localStorage.getItem(key);
//type ReturnType = ???
if (data !== null) {
return data;
}
console.error(`localStorage missing object with key ${key}`);
return null;
},
set(key: Keys, value: any) {
window.localStorage.setItem(key, value);
},
remove(key: Keys) {
window.localStorage.removeItem(key);
},
clear() {
window.localStorage.clear();
},
};
Use generics to return the right type for the key !
type LocalStorageSchema = {
token: string;
some_string: string;
some_number: number;
};
type Keys = keyof LocalStorageSchema;
export const LocalStorage = {
get<T extends Keys>(key: T): LocalStorageSchema[T] | null { // Return type will depend on the key
const data = window.localStorage.getItem(key);
//type ReturnType = ???
if (data !== null) {
return data as LocalStorageSchema[T];
}
console.error(`localStorage missing object with key ${key}`);
return null;
},
set<T extends Keys>(key: T, value: LocalStorageSchema[T]) {
window.localStorage.setItem(key, value as any);
},
remove(key: Keys) {
window.localStorage.removeItem(key);
},
clear() {
window.localStorage.clear();
},
};
const a = LocalStorage.get('token'); // string | null
Playground
You can use generic as in this link. There are still some errors left to fix. Link how to handle null values. It's up to you, so I didn't fix it.

Is there a way to "subscribe" to an array variable changes in Angular?

I'm looking for a way to wait for the user to stop interaction and then make an HTTP request, for this I'm looking into the debounceTime() operator from RxJs, but the target I'm waiting for is an array I defined.
This is the scenario:
export class KeywordSelectionComponent implements OnInit {
constructor(private proposalService: ProposalService) { }
#ViewChild(MatTable, {static: true}) kwTable: MatTable<any>;
#ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
#Input() proposalId: string;
keywordInput = new FormControl(null, Validators.required);
dataSource: MatTableDataSource<Keyword>;
displayedColumns = ['action', 'keyword', 'searches', 'competition', 'cpc'];
suggestedKeywords: Keyword[] = [];
selectedKeywords: string[] = [];
fetchSuggestions(seeds?: string[]) {
const ideas = {
seeds: [],
limit: 25
};
this.proposalService.getKeywordIdeas(this.proposalId, ideas).pipe(retry(3)).subscribe(res => {
this.suggestedKeywords = res;
});
}
}
I'm not including the whole component here, but the idea is the following:
I have a list of suggestedKeywords which I render on the page, each of these should call an addKeyword() method to add that keyword to the dataSource, and after that, I call the fetchSuggestions() method to get new keywords to populate the suggestedKeywords list.
The problem comes when I try to select multiple keywords in quick succession, since that would trigger a request for each of those clicks to update the suggestedKeywords list, so I wanted to use the debounceTime() to prevent the request to trigger until the user stops clicking items for a bit; however this requires an Observable to be the element changing as far as I know, but in my case, it is just a simple array.
Is there someway to keep track of the value of the array so it waits for a while after it changes before making the HTTP request, like an Observable?
EDIT: Used the from() operator as suggested in the comments, in order to actually listen to changes do I need to define other methods? I'm thinking something similar to valueChanges() in FormControls.
Going through more documentation I'm leaning towards Subject, BehaviorSubject, etc; but I'm not sure if this would be a correct approach, could anyone provide an example on how to do this?
Wrap your array in Observable.of() RxJS operator and it will behave like observable
What I ended up doing was using a Subject to keep track of the changes, calling it's next() function evrytime a modified the suggestedKeywords array and subscribing to it as an observable.
My component ended up looking like this:
export class KeywordSelectionComponent implements OnInit {
constructor(private proposalService: ProposalService) { }
keywordInput = new FormControl(null, Validators.required);
suggestedKeywords: Keyword[] = [];
selectedKeywords: string[] = [];
isLoadingResults = false;
tag$ = new Subject<string[]>();
ngOnInit() {
this.tag$.asObservable().pipe(
startWith([]),
debounceTime(500),
switchMap(seeds => this.getSuggestionsObservable(seeds))
).subscribe(keywords => {
this.suggestedKeywords = keywords;
});
}
addSuggestedKeyword(keyword: Keyword) {
const suggestedKeyword = keyword;
const existing = this.dataSource.data;
if (!existing.includes(suggestedKeyword)) {
existing.push(suggestedKeyword);
this.dataSource.data = existing;
}
this.tag$.next(this.getCurrentTableKeywords());
}
fetchKeywordSearch(keyword: string) {
this.isLoadingResults = true;
this.keywordInput.disable();
const search = {
type: 'adwords',
keyword
};
const currentData = this.dataSource.data;
this.proposalService.getKeywordSearch(this.proposalId, search).pipe(retry(3)).subscribe(res => {
currentData.push(res);
this.dataSource.data = currentData;
}, error => {},
() => {
this.isLoadingResults = false;
this.keywordInput.enable();
this.tag$.next(this.getCurrentTableKeywords());
});
}
getCurrentTableKeywords(): string[] {}
getSuggestionsObservable(seeds: string[] = []): Observable<Keyword[]> {
const ideas = {
type: 'adwords',
seeds,
limit: 25
};
return this.proposalService.getKeywordIdeas(this.proposalId, ideas).pipe(retry(3));
}
}

Typescript overloading type boolean is not assignable to type false

I have a method that returns different type based on option key value.
class Test {
getData(options: { obj: true }): Object;
getData(options: { obj: false }): any[];
getData(): any[];
getData(options = { obj: false }): Object | any[] {
if (options.obj) {
return {};
} else {
return [];
}
}
}
When passing the obj as true, I'm returning object otherwise array. That works fine.
const instance = new Test();
const result = instance.getData({ obj: true }); // inffered as array
const result2 = instance.getData(); // inffered as object
The problem is when I need to use dynamic value it throws an error:
type boolean is not assignable to type false
function getResult(obj: boolean = false ) {
return instance.getData({ obj });
}
What is the problem?
Since the type of { obj } is only known as { obj: boolean } at compile time, the compiler can't know to pick any of the overloads, you have to explicitly supply an overload that takes { obj: boolean } (since the implementation signature does not count as a public signature for the function), the compiler will not do any magic in this case:
class Test {
getData(options: { obj: true }): Object;
getData(options: { obj: false }): any[];
getData(options: { obj: boolean }): Object | any[];
getData(): any[];
// This signature is the implementation and is not conidered when resolving the method
getData(options = { obj: false }): Object | any[] {
if (options.obj) {
return {};
} else {
return [];
}
}
}
Edit
You can also use conditional types in the method signature and this will keep the number of signatures lower:
class Test {
getData<T extends boolean>(options: { obj: T }): T extends true ? Object : any[];
getData(): any[];
// This signature is the implementation and is not conidered when resolving the method
getData(options = { obj: false }): Object | any[] {
if (options.obj) {
return {};
} else {
return [];
}
}
}
const instance = new Test();
const result = instance.getData({ obj: true }); // inffered as array
const result2 = instance.getData(); // inffered as object
function getResult(obj: boolean = false) {
return instance.getData({ obj }); // inferred as Object|any[]
}
Since type boolean = true | false and conditional types distribute over unions,
T extends true ? Object : any[]; will be Object|any[] when T is boolean. When T is true, the return will be Object and when T is false, the return will be any all as expected
You cannot overload methods in TypeScript like you do in C#, for example. You need to combine types, like this:
class Test {
getData(options: {obj: boolean} = { obj: false }): Object | any[] {
if (options.obj) {
return {};
} else {
return [];
}
}
}
When you redeclare the function with the same name several times, you will just get the last one as the final definition, in runtime.

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,
}
}

Categories

Resources