TypeScript - How to infer a type from array of objects - reactjs

I am building a React form component with TypeScript and want to type the data returned by my form.
My component accepts a prop called "fields" with the following structure:
const fields = [
{
name: "title",
default: "",
data: undefined,
size: 2
},
{
name: "users",
default: [],
data: [{id: 1, ...}]
size: 8
},
]
I would like to find a way to retrieve the type of the data returned by component based on the "field" variable. So, basically, I want to get something like that:
type FormData = {
title: string;
users: Array<{id: number, ...}>
}
The best solution would be to infer a different type depending on the "data" key. If "data" exists, then the field will have the same type as data, otherwise it is the type of the "default" key. All other keys should be ignored.
I believe I should be using generics to achieve this but I am not even sure this is something possible with TS and I have to admit that I can't figure it out...
Has anyone already faced this situation and found a solution to a similar issue ?
Thank you very much!

Based on the given array fields, we can create the type FormData with the following generic type as long as the fields variable was initialized with as const to stop TypeScript from widening the string literals.
const fields = [
{
name: "title",
default: "",
data: undefined,
size: 2
},
{
name: "users",
default: [],
data: [{id: 1}],
size: 8
},
] as const
type FormData<T extends readonly { name: st
[E in T[number] as E["name"]]: E["data"]
? E["default"]
: E["data"]
}
type Result = FormData<typeof fields>
// type Result = {
// title: "";
// users: readonly [{
// readonly id: 1;
// }];
// }
This might or might not work in the context of your component. But you did not show us the component itself. This is a solution based on the information given in your question.
Playground

Related

How to infer types of an object created from a schema?

I'm trying to implement something similar to storybook's "Controls" feature where you can define a bunch of properties, and they become controls in the UI.
I defined a schema type and a schema of how to create those controls:
// Example schema
var newSchema: BlockSchema = {
title: "New Schema",
controls: {
name: {
type: 'string',
placeholder: 'Please insert your name'
},
size: {
type: 'select',
options: ['quarter', 'half', 'full']
},
hasInfo: {
type: 'bool'
},
amount: {
type: 'number'
}
}
}
But now I need a type that is the result of what the user has selected. A type for the final values, something like:
type MapControlTypes = {
bool: boolean;
string: string;
select: string;
number: number;
};
type InferType<T extends BlockSchema> = { /* MapControlTypes<?????????> */ }
type NewSchemaControls = InferType<typeof newSchema>;
/* Expected result:
NewSchemaControls = {
name: string;
size: string;
hasInfo: boolean;
amount: number;
}
*/
I need to infer the types from the controls property of my schema, but how could I implement this inference? Here's a playground with complete example code
I tried implementing this, and this solution. But they don't work well and also only support two types.
Titian Cernicova-Dragomir's solution didn't work too. Playground, but it has a very similar problem that happened when I tried other solutions. Maybe is it because I'm not using MapControlTypes on my ControlSchema?
Solved!
You can do this, using a mapped type, but first you need to preserve the original type of the schema. If you add a type annotation to it, then information about specific fields and types will be lost. It will just be a BlockSchema
The easiest way to do this is to omit the annotation, and use an as const assertion to make the compiler infer literal types for type.
With this extra info in hand, we can then use a mapped type to transform the schema into an object type:
type InferType<T extends BlockSchema> = {
-readonly [P in keyof T['controls']]: MapControlTypes[T['controls'][P]['type']]
}
Playground Link
You can also use a function to create the schema, and be more selective about what gets the readonly treatment:
function buildBlockSchema<B extends BlockSchema>(b: B) {
return b
}
Playground Link

Different object fields in Typescript based on conditional type

I want to write a function (eventual use is a React function component) in Typescript that takes a props object with a list of list of objects of any type. Assume the function should print a "key" for each, where if the object type has an id field then the id value is printed as the key, and if the object type doesn't have an id field, the key will be derived from an accessor function in the props object (pseudocode here with types omitted):
function process(props: { items: ..., ...}) {
props.items.forEach(item => {
if (item.id) {
console.log(`Key for ${item.id}`)
} else {
console.log(`Key for ${props.keyFunction(item)}`)
}
})
}
process({items: [{id: "1", name: "A"}, {id: "2", name: "B"}]})
process({items: ["A", "B"], keyFunction: (item) => item})
Ideally, I'd like the following:
Typescript should error if keyFunction is provided but the items already have an id
Typescript should error if the items don't have an id and keyFunction isn't provided
Typescript should know about keyFunction and id in the appropriate places inside the process function body (autocomplete should work)
Is there a way to write the types for this function so that all 3 of the above work?
Note: I understand that if these were parameters instead of values of a config object, I could use conditional function overloads, but because the actual use case for this is a React function component with props, that won't work here.
What I've tried
I've tried using a conditional type, which works at the callsite, but I can't figure out how to make Typescript know about keyFunction correctly playground link:
type KeyProps<T> = T extends { id: string }
? { items: T[] }
: {
items: T[];
keyFunction(item: T): string;
};
function process<T>(props: KeyProps<T>) {
props.items.map(item => {
if (item.id) {
console.log(`Key for ${item.id}`)
} else {
console.log(`Key for ${props.keyFunction(item)}`)
}
})
}
I've also tried using a discriminated union, but I don't know how to provide a type constraint to only one branch of the union with a generic:
type KeyProps<T> =
| { type: "autokey", items: T[] } // How to provide that T should extend { id: string } here?
| { type: "normal", items: T[], keyFunction(item: T): string }
Note that this answer assumes that it isn't important that the two parameters are contained inside of a wrapper object.
Ideally, I'd like the following:
Typescript should error if keyFunction is provided but the items already have an id
Typescript should error if the items don't have an id and keyFunction isn't provided
Typescript should know about keyFunction and id in the appropriate places inside the process function body (autocomplete should work)
Okey, requirement two (2) and three (3) can be solved by simply using function overloads and some cleverly selected default values (you will see that we always call keyFunction and that this is actually a good idea).
However, requirement one (1) is quite tricky. We can easily infer the type of items using a generic T. Then using T we can derive a type D such that D does not contain any object types with a key of id. The tricky part is managing to both derive the base type T from items while also constraining the type of items to D.
The way I've done it is by using an intersection type T[] & D[] for the type of items. This will infer the base type T from elements in the items array, while also constraining the type of the elements in the items array.
interface ObjWithID {
[k: string]: any;
id: string;
}
type AnyWithoutID<T> = T extends { id: any } ? never : T;
function process(items: ObjWithID[]): void;
function process<T, D extends AnyWithoutID<T>>(items: T[] & D[], keyFunction: (value: T) => string): void;
function process(items: any[], keyFunction: (value: any) => string = ({id}) => id) {
items.forEach(item => console.log(`Key for ${keyFunction(item)}`))
}
process([{id: "1", name: "A"}, {id: "2", name: "B"}])
process([{id: "1", name: "A"}, {id: "2", name: "B"}], (item) => item)
process([{ name: "A"}, { name: "B"}], (item) => item.name)
process(["A", "B"], (item) => item)
playground
Note that there is a rather annoying drawback when messing with the type system this much. The type errors end up quite cryptic. For instance process([{id: "1", name: "A"}, {id: "2", name: "B"}], (item) => item) will throw the error Type 'string' is not assignable to type 'never'. for both id & name in both objects. These errors can be really annoying to debug, so make sure you absolutely need this kind of overload behavior before you commit to it fully.

Formatting data from a database in TypeScript

I am having trouble with writing the following method on an Angular class. I don't know how to add values from arrayId to the data array in the series object.
getChartOptions() {
const arrayId=[];
const arrayTimestamp=[];
const arrayData=[];
const arrayData2=[];
var i=0;
this.httpClient.get<any>('http://prod.kaisens.fr:811/api/sleep/?deviceid=93debd97-6564-454b-be33-35bd377a2563&startdate=1612310400000&enddate=1614729600000').subscribe(
reponse => {
this.sleeps = reponse;
this.sleeps.forEach(element => { arrayId.push(this.sleeps[i]._id),arrayTimestamp.push(this.sleeps[i].timestamp),arrayData.push(this.sleeps[i].data[18]),arrayData2.push(this.sleeps[i].data[39])
i++;
});
console.log(arrayId);
console.log(arrayTimestamp);
console.log(arrayData);
console.log(arrayData2);
}
)
return {
series: [{
name: 'Id',
data: [35, 65, 75, 55, 45, 60, 55]
}]
}
}
I have two main pieces of advice for you:
Know the types of that data that you are dealing with.
Get familiar with all of the various array methods.
get<any>() is not a helpful type. If you understand what the response is then Typescript can help ensure that you are handling it correctly.
I checked out the URL and it looks like you get an array of objects like this:
{
"_id": 4,
"device_id": "93debd97-6564-454b-be33-35bd377a2563",
"timestamp": 1612310400000.0,
"data": "{'sleep_quality': 1, 'sleep_duration': 9}"
},
That data property is not properly encoded as an object or as a parseable JSON string. If you control this backend then you will want to fix that.
At first I thought that the data[18] and data[39] in your code were mistakes. Now I see that it as attempt to extract values from this malformed data. Accessing by index won't work if these numbers can be 10 or more.
The type that you have now is:
interface DataPoint {
_id: number;
device_id: string;
timestamp: number;
data: string;
}
The type that you want is:
interface DataPoint {
_id: number;
device_id: string;
timestamp: number;
data: {
sleep_quality: number;
sleep_duration: number;
}
}
You can type the request as this.httpClient.get<DataPoint[]>( and now you'll get autocomplete on the data.
It looks like what you are trying to do is basically to convert this from one array of rows to a separate array for each column.
You do not need the variable i because the .forEach loop handles the iteration. The element variable in the callback is the row that you want.
this.sleeps.forEach(element => {
arrayId.push(element._id);
arrayTimestamp.push(element.timestamp);
arrayData.push(element.data[18]);
arrayData2.push(element.data[39]);
});
The .forEach loop that you have now is efficient because it only loops through the array once. A .map for each column is technically less efficient because we have to loop through separately for each column, but I think it might make the code easier to read and understand. It also allows Typescript to infer the types of the arrays. Whereas with an empty array you would need to annotate it like const arrayId: number[] = [];.
const mapData = (response: DataPoint[]) => {
return [{
name: 'Id',
data: response.map(element => element._id)
}, {
name: 'Timestamp',
data: response.map(element => element.timestamp)
}, {
name: 'Sleep Quality',
data: response.map(element => parseInt(element.data[18])) // fix this
}, {
name: 'Sleep Duration',
data: response.map(element => parseInt(element.data[39])) // fix this
}]
}
The HTTP request is asynchronous. If you access your array outside of the subscribe callback then they are still empty. I'm not an angular person so this part I'm unsure of, but I think that you want to be updating a property on your class instead of returning the value?
Just follow this piece of code:
series: [{
name: 'Id',
data: arrayId
}]

How to push a new value to an array if the current value is an array or set the value as an array if it is not in a single MongoDB Query

I have a project where we have been using simple, unversioned values for documents:
{
_id: <someid>,
prop1: 'foo',
prop2: 'bar',
prop3: 'baz'
}
I would like to update the method that saves prop values to start saving values as versions in an array, to look like this:
{
_id: <someid>,
prop1: [{ value: 'foo', createdAt: <someDate>}],
prop2: [{ value: 'bar', createdAt: <someDate>}, { value: 'barrrrr', createdAt: <someDate>}],
prop3: 'baz'
}
I would like, in my update query, to $push the new prop value object if it's already an array, or to $set it to `[{ value: 'newvalue', createdAt: +new Date()}] if not. Ideally, this would let me seamlessly transition the data to be versioned over time. On the retrieval side, if it's not an array we just treat the only value that's there as the reference version, and whenever anything gets updated, that prop is converted to the new format.
I've been struggling to find an example of that same use case: can anyone point me in the right direction?
UPDATE:
After being pointed in the right direction, I was able to use the aggregation pipeline in combination with update to do what I wanted. Part of the key was to abandon trying to pivot between setting and pulling--instead, I could use the helper method $concatArrays to accomplish the array addition a different way. Here's the basic shell code I got to work, purely to show the structure:
db.test.docs.update({ key: 2 }, [
{
$set: {
prop2: {
$cond: {
if: { $isArray: '$prop2' },
then: {
$concatArrays: [
'$prop2',
[
{
value: 'CONCAT!'
}
]
]
},
else: [
{
value: 'SET!'
}
]
}
}
}
}
]);
In MongoDB 4.2 you can use the pipeline form of update to use aggregation stages and operators to do that.
You would likely need to use $cond and $type to find out if the field already contains an array, and then $concatArrays to combine the values.

How can I get an item in the redux store by a key?

Suppose I have a reducer defined which returns an array of objects which contain keys like an id or something. What is the a redux way of getting /finding a certain object with a certain id in the array. The array itself can contain several arrays:
{ items:[id:1,...],cases:{...}}
What is the redux way to go to find a record/ node by id?
The perfect redux way to store such a data would be to store them byId and allIds in an object in reducer.
In your case it would be:
{
items: {
byId : {
item1: {
id : 'item1',
details: {}
},
item2: {
id : 'item2',
details: {}
}
},
allIds: [ 'item1', 'item2' ],
},
cases: {
byId : {
case1: {
id : 'case1',
details: {}
},
case2: {
id : 'case2',
details: {}
}
},
allIds: [ 'case1', 'case2' ],
},
}
Ref: http://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html
This helps in keeping state normalized for both maintaining as well as using data.
This way makes it easier for iterating through all the array and render it or if we need to get any object just by it's id, then it'll be an O(1) operation, instead of iterating every time in complete array.
I'd use a library like lodash:
var fred = _.find(users, function(user) { return user.id === 1001; });
fiddle
It might be worth noting that it is seen as good practice to 'prefer objects over arrays' in the store (especially for large state trees); in this case you'd store your items in an object with (say) id as the key:
{
'1000': { name: 'apple', price: 10 },
'1001': { name: 'banana', price: 40 },
'1002': { name: 'pear', price: 50 },
}
This makes selection easier, however you have to arrange the shape of the state when loading.
there is no special way of doing this with redux. This is a plain JS task. I suppose you use react as well:
function mapStoreToProps(store) {
function findMyInterestingThingy(result, key) {
// assign anything you want to result
return result;
}
return {
myInterestingThingy: Object.keys(store).reduce(findMyInterestingThingy, {})
// you dont really need to use reduce. you can have any logic you want
};
}
export default connect(mapStoreToProps)(MyComponent)
regards

Resources