Typescript, conditional type based on the value of a string literal type - reactjs

I'm a little stuck on how to define a conditional type for typescript based on a predefined string literal
This is a mockup / example to help explain For a little background, a user is supposed to label data from images, currently there are only 2 types of things to label (for this example lets say its pedestrians and vehicles) potentially more in the future.
In my Redux store I am storing data from an API request (request gets a new set of data to label based on one of these types). The issue is the data that i get is in different formats for the two types of things to label aka array of frames or object with values that are frame arrays.
type ILabelingTypes = "pedestrians" | "vehicles"
A frame would look like this
interface IFrame {
url: string
orientation: number
// anything else related to a frame
}
The api requests would be /labeling/vehicles/new with an example response
{
// meta info about the labeling session
frames: IFrame[]
}
or /labeling/pedestrians/new with an example response
{
// meta info about the labeling session
frames: Map<string, IFrame[]>
}
The issue I'm having now is when I define my store interface, how do i correctly type the frames key so that I don't have to check everywhere i go to use it what type of data it is?
interface ILabelingStore {
labelingType: ILabelingTypes
frames: // what do i put here?
}
Meaning when i go to render or use this data I'd like to simply call methods that i know exist depending on the labelingType defined in the store
For the React component side, when i go to render the frames I build a queue of frames to complete, so i need to know what type the data is in the respective component (This is what i would like to do instead of having to check the type of frames)
// pedestrians component
componentDidMount() {
Object.entries(this.props.frames).forEach( (key, val) => this.queue.concat(val))
}
// vehicles component
componentDidMount() {
this.queue.concat(this.props.frames)
}

You can create a discriminated union:
type ILabelingStore = ({
labelingType: "vehicles",
frames: IFrame[]
} | {
labelingType: "pedestrians",
frames: { [key: string]: IFrame[] }
});
Demo

Related

TS: Common function with objects that have the same structure but different possible values on a property

So, we have an app with multiple resources, let's say we have Product, Cart, Whatever resources. For each of those resources you can create activities, the main idea here is that for each resource there is an endpoint to create/update those activities, which looks the same no matter the resource you are trying to update.
So in our app (React) we created a single form to create/update an activity, it looks the same no matter for which resource you want to create an activity for, same fields, same possible values. Therefore we have one single component instead of 3, and a common function that handles the api part.
Something like:
const { mutate } = useUniversalEditActivity(variant); // variant can be 'product', 'cart', 'whatever'
We call mutate when we want to submit the form.
Inside that hook, there is a simple map:
const variantMapper = {
product: {
serviceAction: updateProductActivity, // simple function that wraps fetch, does the network request
},
cart: {
serviceAction: updateCartActivity,
},
whatever: {
serviceAction: updateWhateverActivity,
},
};
// Using it like
const mutatingServiceAction = variantMapper[variant].serviceAction;
...
await mutatingServiceAction();
The body is typed as
type UniversalEditActivityBodyType =
| UpdateProductActivityRequestBody
| UpdateCartActivityRequestBody
| UpdateWhateverActivityRequestBody
Which works when all the properties are the same across the types, but the problem starts now when the BE changed the spec for the Whatever resource.
So, before the request body had a property which had 2 possible values, so it was typed like:
type UpdateProductActivityRequestBody = {
propertyWithIssues: 'a'| 'b';
}
All 3 looked the same, but the spec changed for the Whatever resource to:
type UpdateWhateverActivityRequestBody = {
propertyWithIssues: 'a'| 'b' | 'c' | 'd';
}
Adding 2 more possible values for the same property, now there is a difference on how they look and inside my generic function that handled all body types, now I get the Type '"a" | "b" | "c" | "d"' is not assignable to type '"a" | "b"'.
I kind of understand the error, but not sure how to fix it in order for my function to still work with all those 3 types when just the possible values on a single property is different between them.
I don't know if I explained as good as I should have, but it's a more complex question (I think), so I tried my best. Please also suggest a different title if you think it would better describe my problem. Thanks in advance.
UPDATE 1:
#chris-hamilton, the request is executed like this:
const resp = await mutatingServiceAction(id, activityId, payload);
This is where the issue happens, because payload is a union of all those 3 types, but now they have become incompatible.
Minimum reproducible example here: https://codesandbox.io/s/gallant-bessie-sxjc4x?file=/src/index.ts
I know in theory that the issue could be solved by doing something like:
if (variant === 'product') {
// directly use product function
}
...
But I have the feeling this can be something different, as they have the exact same structure, just one property can have different values.
This error is really just a big red flag for this design pattern. You've specified that these two parameters can be one of many types, meaning you can have any combination of variant and body. ie. this function would accept a variant of "product" and a body of UpdateCartActivityRequestBody. But clearly that is not how the function is meant to be used.
It's a question of what these updateActivity functions are doing, and how similar they are. It may be that the parameters of these functions can accept a common type:
type Body = {
propertyWithIssues: string;
}
function main(
variant: "product" | "cart" | "whatever",
body: Body
) {
const serviceAction = variantMapper[variant].serviceAction;
serviceAction(body); // no error
}
So you need to ask yourself, "do my functions need to know about all these specific properties?".
If the answer is no, then define a type with only the properties needed for those functions. If those properties are common for all functions then the design pattern is fine. The type may just be Object, it depends what the functions are doing.
If the answer is yes, then the design pattern is incorrect, and you shouldn't be coupling these service actions into a single function. You should just be calling updateWhateverActivity directly, with the correct parameter type. No need for this variantMapper object.
Maybe you have other reasons for implementing this pattern, but you'll need to give more details if that's the case.
I can see it maybe being the case that you have an object with variant and body and you don't know what they are until run time. In that case you will have to do type narrowing like you showed. But you should also be checking that the type of body actually matches variant or you're just asking for runtime errors. This is really what the error is trying to tell you.
if (variant === 'product') {
// somehow verify that body is of type UpdateProductActivityRequestBody
// directly use product function
}

Allow temporarily empty object for a typed state

I'm coming from JS background and learning Typescript now. I can't get over this issue.
I have a very particularly typed state that I know I will need in the future to work with:
type NotificationValuesType = {
channel: string
for: NotificationUsageTypes
id: string
type: NotificationTypes
workspace: string
}[]
I'm setting my React state like this:
const [dropdownsState, setDropdownsState] = useState<NotificationValuesType>([])
The thing is that initially I can now only type and id, all of the rest of props I can gather thru a series of dropdowns when a user chooses them, event fires, and then populates the state with one dropdown at a time, so at some point, it will be only:
[{id: "id", type: "type", channel: "channel"}]
at the next event, it will be [{id: "id", type: "type", channel: "channel", workspace: "workspace"}] and one more step and state update to get to know all the props in the declared type.
What I don't understand is how to tell Typescript to stop yelling at me until the point I know all the props and make sure I will know all the needed props in the future.
I absolutely cannot make any of these props optional because they
aren't optional.
Also tried setting state type to NotificationValuesType | [] but typescript keeps yelling
I learned about type assertions and guess it should help but can't find any example of using it on the state. Can I do something
like const [dropdownsState, setDropdownsState] = useState as <NotificationValuesType>([]) ???
Thanks for reading all way to the end! =)
I absolutely cannot make any of these props optional because they
aren't optional.
Perhaps not when you're done, but while you're in the process of building it, if values need to be undefined, then the type needs to reflect that. So most likely you'll want to have the state variable have optional properties, then you go through your steps to fill it out, and once you've verified it's all there you can assign it to a variable with a type where the properties are all mandatory.
There is a helper type called Partial which will take in a type and produce a new one who's properties are optional.
// Note: this type is just the individual object, not the array that you have
type Example = {
channel: string
for: NotificationUsageTypes
id: string
type: NotificationTypes
workspace: string
}
const [dropdownsState, setDropdownsState] = useState<Partial<Example>[]>([])
// Later on, once you've verified that all the properties exist you can
// assert that it's done. I don't know exactly what your verification
// code will look like, but here's an example
if (dropdownsState.every(value => {
return value.channel && value.for && value.id && value.type && value.workspace
})) {
const finishedState = dropdownsState as Example[];
// do something with the finished state
}
EDIT: as pointed out in the comments, if you use a type guard, then typescript can narrow down the types and save you from having to reassign it:
if (dropdownsState.every((value): value is Example => {
return value.channel && value.for && value.id && value.type && value.workspace
})) {
// Inside this block, typescript knows that dropdownsState is an Example[]
}

map big json object to model object in typescript

I have array of object, each object contains;
{
Id:"..",
name:"..",
foo1:"..",
foo2:"..",
...
}
I need only 2 properties of these items so I have created an interface;
export interface IMenuModel{
Id:number;
name?:string;
}
and retrieve data within this method below:
fetch(`..`).then((response: Response): Promise<{ value:IMenuModel[] }> => {
return response.json();
})
.then((response: { value: IMenuModel[] }): void => {
debugger //expected response is array of IMenuModel but it still contains all properties
I expect this response object as array of my custom model(IMenuModel) but it still contains all properties retrieved from remote source.
I can pluck them with ".map()" function thats ok but there is return type defined to function(response: { value: IMenuModel[] }) so I shouldnt have to do this(or do I have to map it manually each time).
Why still response not in my object model and whats the most efficient way to achieve this?
TypeScript is mostly a type layer on top of JavaScript. These typings do not change the typed object. When transpiled into JavaScript the typings are simply dropped. If you apply an interface (or multiple interfaces) to your JSON object, you don't change the nature of the object, you just declare the guarantees of that interface.
So when you define your IMenuModel interface and apply it to your imported object you are saying: "I know and I can guarantee that the given object provides the members defined in the interface." This guarantee is satisfied by your object, so everything is fine there. The object however is still transmitted and restored in its full glory. Consider the interfaces as minimum guarantees, not as full descriptions of the object.
If you don't want the additional members of the object, your only choice is to let the server send only the parts of the object you need. If you can't do that, you don't need to worry, as your object meets the minimum requirements of the interface. Just ignore the additional values.

Relay mutation. FatQuery. Ask all fields in REQUIRED_CHILDREN

My question is: i have a mutations config where i have a REQUIRE_CHILDREN config with children array of queries. How can i get all possible fields from a payload object?
{
type: 'REQUIRED_CHILDREN',
children: [
Relay.QL`
fragment on MyPayload {
me {
id
...others field
}
}`]
So how can i ask all possible fields from me object? If i point only fragment on MePayload { me } object relay still returns me me { id }. I want relay to return me all fields in me object. Thanks.
You can't - your client code needs to specify all the fields you want to fetch explicitly. Those fields are then statically validated by the babel-relay-plugin, etc.
You probably don't want to be using REQUIRED_CHILDREN either, by the way. That's only useful to fetch fields that are only accessible in the onSuccess callback of the mutation, and therefore are never written to the Relay store and accessible to Relay containers...

In Typescript/Angular 2 how to find an object in an array by a property of the object

I've been doing some searching and there seem to be a few possible solutions.
First one that will probably work: Filtering an array in angular2
but I find using a pipe and a for loop not ideal.
My idea was using an interface.
This would be a typescript solution. Problem here is defining the collection.
public covers = COVERS;
public images = VECTORS; // a BIG image collection
imagesByCoverType: CoverVector = {}; // Array is made if
// I use Vector[]; but I wanted to use interface.
// images are correctly filtered to the relevant ones.
ngOnInit() {
this.imagesByCoverType = this.images.filter(
image => image.type === 'book-cover');
}
// defined outside of the component class of course.
interface CoverVector {
[book_id: number]: Vector;
}
<li *ngFor="let cover of covers")>
<p>{{cover.title}}</p>
<!-- Here I would like to print out a url to an image of an image object -->
<!-- cover has a property book_id, and also a
chapter_id because the book is subdivided -->
<!-- I was thinking something like this: -->
<p>{{imagesByCoverType[cover.id].url}}</p>
</li>
So I want to access an object in an array by using an interface. How do I do this? Considering also that I have a filtered array of vectors that it should go over.
Recap for clarity:
I want:
A big collection of data that has a unique identifier connected to an 'interface' or find method.
This interface should make it possible to input this unique id and access with it the desired object.
Then all properties of this object should be accessible. Or actually only the url in this case. Point being: it should be there, not just the interface property, but any desired object property.
Then all this elegantly wrapped up in a Angular 2 ngIf statement.
I wouldn't have thought this in-array-find-by-object-property thing would be so hard but it's been a struggle.
Solution I am currently using
It's beyond me why I have to resort to a entire for loop just to access one element I already know the identifier of - from a for loop above it - but I used a for loop using a custom pipe.
// Angular 2
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'cover'
})
export class CoverVectorPipe{
transform(value: any, cover_id: number) {
return value.filter(
(item: any)=> item.aim_id === cover_id);
}
}
Any help solving this using an interface is still welcome.
Also I am wondering if this isn't computationally expensive.

Resources