Object must NOT have key - flowtype

I am trying to define that an object must NOT have a certain key.
Here is my case:
alert({
items: [{ label:'Apple' }, { label:'Orange' }]
})
alert({
items: [{ foo:'Apple' }, { foo:'Orange' }]
labelKey: 'foo'
})
If items is an array of ojects that does not contain "label" key, then labelKey is required in Options
I tried this:
type Options = {|
items: Array<{ label:string }>
|} | {|
items: Array<$Diff<{}, { label:string }>>,
labelKey: string // must be key in items
|}
function alert(options: Options) {
}
Bonus question:
Also is it possible to define that labelKey is any key from objects passed in items?

Ensuring a property does not exist on an object
tl;dr: use { myProp?: empty }
I'm assuming you want to use the objects as maps when you pass something into the alert function. The trick to creating a map without a label is to give a property that, if assigned to something, will fail to typecheck.
We can leverage the empty type, a type which doesn't match against anything, to get the desired effect. It's a little tricky to use an empty type in conjunction with an object map, because by defining a property, we tell flow that we want that type to be in the object. So this fails to typecheck:
(Try)
type MapWithLabel = {
[string]: string,
label: string,
}
type MapWithoutLabel = {[string]: mixed, label: empty}
type Options = {|
items: Array<MapWithLabel>
|} | {|
labelKey: string,
items: Array<MapWithoutLabel>,
|}
declare function alert(options: Options): void;
alert({
items: [{ foo:'Apple' }], // Error, expected a "label" property with empty type
labelKey: 'foo'
})
Next, we can define the property as optional, which means to only typecheck against empty if the property exists. With this we can give the object a "label" property that either:
Does not exist OR
Has a type that matches nothing (empty)
So code can either not have a value for that property (what we want), or it can pass something that is empty (this isn't possible).
(Try)
type MapWithLabel = {
[string]: string,
label: string,
}
type MapWithoutLabel = {[string]: mixed, label?: empty}
type Options = {|
items: Array<MapWithLabel>
|} | {|
labelKey: string,
items: Array<MapWithoutLabel>,
|}
declare function alert(options: Options): void;
alert({
items: [{ label:'Apple' }],
})
alert({
items: [{ label:'Apple' }], // Error - Should not have label
labelKey: 'ohno',
})
alert({
items: [{ foo:'Apple' }],
labelKey: 'foo'
})
alert({
items: [{ foo:'Apple' }], // Error - Needs a labelKey
})
So to get the desired effect, we needed to leverage two tools: optional properties and the empty type. With it, we can specify an object that will fail to typecheck if that empty property exists.
Setting a dynamic property key at the type level
tl;dr: not possible
Regarding the bonus question: I'm not sure Flow could understand that since I don't know of a way to set a variable property on objects. I would not expect this feature since it could make things complicated/impossible to type check.
Edit: After a little more research, you can use indexer properties to assert an object has a key at the type level:
(Try)
type ObjWithKey<T: string = 'label'> = {
// An indexer property with only one valid value: T with "label"
// as default, but we can't ensure that the property exists anymore
// and multiple indexers are not supported.
[T]: string,
aNumber: 3,
aFunction: () => void,
}
declare var usesLabel: ObjWithKey<>
(usesLabel.label: string);
(usesLabel.aNumber: number);
(usesLabel.missing: number); //Error - Doesn't exist on object
(usesLabel.aFunction: () => void);
(usesLabel.aFunction: string); //Error - Wrong type
However, you can't do that and use the object as a general map, since multiple indexer properties are not supported (Try). For reference, someone else tried to do something else similar, but couldn't get it to work.
If that's a major problem for you, see if you can architect your data structure in a different way to make it easier for static analysis with Flow.

Related

DynamoDB update - "ValidationException: An operand in the update expression has an incorrect data type"

I am trying to append to a string set (array of strings) column, which may or may not already exist, in a DynamoDB table. I referred to SO questions like this and this when writing my UpdateExpression.
My code looks like this.
const AWS = require('aws-sdk')
const dynamo = new AWS.DynamoDB.DocumentClient()
const updateParams = {
// The table definitely exists.
TableName: process.env.DYNAMO_TABLE_NAME,
Key: {
email: user.email
},
// The column may or may not exist, which is why I am combining list_append with if_not_exists.
UpdateExpression: 'SET #column = list_append(if_not_exists(#column, :empty_list), :vals)',
ExpressionAttributeNames: {
'#column': 'items'
},
ExpressionAttributeValues: {
':vals': ['test', 'test2'],
':empty_list': []
},
ReturnValues: 'UPDATED_NEW'
}
dynamo.update(updateParams).promise().catch((error) => {
console.log(`Error: ${error}`)
})
However, I am getting this error: ValidationException: An operand in the update expression has an incorrect data type. What am I doing incorrectly here?
[Update]
Thanks to Nadav Har'El's answer, I was able to make it work by amending the params to use the ADD operation instead of SET.
const updateParams = {
TableName: process.env.DYNAMO_TABLE_NAME,
Key: {
email: user.email
},
UpdateExpression: 'ADD items :vals',
ExpressionAttributeValues: {
':vals': dynamo.createSet(['test', 'test2'])
}
}
A list and a string set are not the same type - a string set can only hold strings while a list may hold any types (including nested lists and objects), element types don't need to be the same, and a list can hold also duplicate items. So if your original item is indeed as you said a string set, not a list, this explains why this operation cannot work.
To add items to a string set, use the ADD operation, not the SET operation. The parameter you will give to add should be a set (not a list, I don't know the magic js syntax to specify this, check your docs) with a bunch of elements. If the attribute already exists these elements will be added to it (dropping duplicates), and if the attribute doesn't already exit, it will be set to the set of these elements. See the documentation here: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-UpdateExpression

Add to list only if string doesn't already exist in DynamoDB table

I'm trying with the following code
{
ExpressionAttributeNames: {
"#items": "items"
},
ExpressionAttributeValues: {
":item": [slug]
},
Key: {
listId: listId,
userId: userData.userId,
},
UpdateExpression: "SET #items = list_append(#items,:item)",
ConditionExpression: "NOT contains (#items, :item)",
TableName: process.env.listsTableName,
}
but the item is still added even if string already exists in the list. What am I doing wrong?
The list structure is like so:
{
Item: {
userId: userData.userId,
listId: crypto.createHash('md5').update(Date.now() + userData.userId).digest('hex'),
listName: 'Wishlist',
items: [],
},
TableName: process.env.listsTableName,
};
Later Edit: I know I should use SS as it does the condition for me but SS doesn't work in my context because SS can't be empty.
As the documentation explains, the contains() function only works on a string value (checking for a substring) or a set value (checking for membership). But in your case, you don't have a set but rather a list - with are different things in DynamoDB.
If all the items which you want to add to this list are strings, and you anyway don't want duplicates in the list, the most efficient way would be to stop using a list, and instead use the set-of-strings (a.k.a. SS) type. To add an item to the set (without duplicates), you would simply use "ADD #items :item" (no need for any additional condition - duplicates will not be added).

How to avoid using literal strings to narrow disjoint unions in flow

All the examples I find online for narrowing the disjoint union in flowtype uses string literals, like the official one. I would like to know if there is a way to check against a value from an enum like:
const ACTION_A = 'LITERAL_STRING_A';
const ACTION_B = 'LITERAL_STRING_B';
type ActionA = {
// This is not allowed
type: ACTION_A,
// type: 'LITERAL_STRING_A' is allowed
dataA: ActionAData,
}
type ActionB = {
// This is not allowed
type: ACTION_B,
// type: 'LITERAL_STRING_B' is allowed
dataB: ActionBData,
}
type Action = ActionA | ActionB;
function reducer(state: State, action: Action): State {
// Want to narrow Action to ActionA or ActionB based on type
switch (action.type) {
// case 'LITERAL_STRING_A': -- successfully narrow the type
case ACTION_A: // doesn't work
// action.dataA is accessible
...
}
...
}
Unfortunately you can't do these because strings are ineligible as type annotations.
If there is any other way around this that doesn't force typing the string literals everywhere I would love to know.
If there isn't a way around this, also accept suggestions on a higher level how to not need to define these disjoint sets for redux actions.
I'm not in my best shape right now, so sorry if I read your question wrong. I'll try to help anyway. Is this what you're looking for?
const actionTypes = {
FOO: 'FOO',
BAR: 'BAR'
}
type ActionType = $Keys<actionTypes> // one of FOO, BAR
function buzz(actionType: ActionType) {
switch(actionType) {
case actionTypes.FOO:
// blah
}
This should work. Sorry if my syntax is a bit off.
If you're asking how to avoid listing all action types in type Action = ActionA | ActionB then sorry, I don't know, I think this is the way you do it. If I recall correctly, a slightly nicer syntax for defining long unions was recently introduce in Flow:
type Action =
| ActionA
| ActionB
| ActionC
Also, if you don't need individual action types, you can just do
type Action =
| {type: ACTION_A; dataA: ActionAData;}
| {type: ACTION_B; dataB: ActionBData;}
The better way would be to use string literal types for const values:
Try flow...
const ACTION_A:'LITERAL_STRING_A' = 'LITERAL_STRING_A';
const ACTION_B:'LITERAL_STRING_B' = 'LITERAL_STRING_B';

autoform won't render select option field

I have an issue regarding collection2 with relationships and autoform.
I try to implement an 1:n relationship, where each object has exactly 1 objectType, while to each objectType multiple objects can be referred to.
My schema looks as follows:
// register collections
Objects = new Mongo.Collection('objects');
ObjectTypes = new Mongo.Collection('objectTypes');
// define schema
var Schemas = {};
Schemas.ObjectType = new SimpleSchema({ // object type schema
name: {
type: String
}
});
Schemas.Object = new SimpleSchema({ // object schema
type: {
type: ObjectTypes.Schema,
optional: true
},
title: {
type: String
}
});
// attach schemas
ObjectTypes.attachSchema(Schemas.ObjectType);
Objects.attachSchema(Schemas.Object);
My autoform looks like this:
{{> quickForm collection="Objects" id="insertTestForm" type="insert"}}
I actually would expect a select option field for my type attribute, however, a text input appears. Anyone knows why?
According to the documentation [1], it should be a select option field:
If you use a field that has a type that is a Mongo.Collection instance, autoform will automatically provide select options based on _id and name fields from the related Mongo.Collection. You may override with your own options to use a field other than name or to show a limited subset of all documents. You can also use allowedValues to limit which _ids should be shown in the options list.
[1] https://github.com/aldeed/meteor-collection2/blob/master/RELATIONSHIPS.md#user-content-autoform
EDIT
If I use
type: ObjectTypes,
instead of
type: ObjectTypes.Schema,
my app crashes, throwing the following error:
Your app is crashing. Here's the latest log.
/Users/XXX/.meteor/packages/meteor-tool/.1.1.3.ik16id++os.osx.x86_64+web.browser+web.cordova/mt-os.osx.x86_64/dev_bundle/server-lib/node_modules/fibers/future.js:245
throw(ex);
^
RangeError: Maximum call stack size exceeded
Exited with code: 8
Your application is crashing. Waiting for file change.
Your type isn't "a Mongo.Collection instance" like the documentation says; it's a Schema. Try this:
Schemas.Object = new SimpleSchema({
type: {
type: ObjectTypes,
optional: true
},
...
Since nobody could help me solve this incident, I came up with an alternate solution:
// register collections
Objects = new Mongo.Collection('objects');
ObjectTypes = new Mongo.Collection('objectTypes');
// define schema
var Schemas = {};
Schemas.Object = new SimpleSchema({ // object schema
type: {
type: String,
optional: true,
autoform: {
return ObjectTypes.find().map(function(c) {
return{label: c.name, value: c._id}
});
}
},
// ...
});
// attach schema
Objects.attachSchema(Schemas.Object);
As u can see, I manually map the attributes I need from the objectTypes collection into the autoform attribute. Since it returns an array of objects, containing the label and value attributes, autoform will automatically render a select option.

Some questions on Simple-Schema for Meteor

For those who use Simple Schema for Meteor validation I've got some questions (after having used it for a while):
1.
Does SimpleSchema support square bracket notation? All the examples show it using dot notation. I did the following square bracket implementation:
Schemas.NamesCollection = new SimpleSchema({
name: {
type: Object
},
"name[first_name]" : {
type: String,
optional: false,
label: "First Name"
},
"name[last_name]" : {
type: String,
optional: false,
label: "Last Name"
}
});
{{> afQuickField name="name[first_name]"}}
{{> afQuickField name="name[last_name]"}}
I noticed that the form validation worked just fine as I'm filling out the form, but the final insertion of the data into the collection fails with a validation error (the same exact error that had passed earlier on the form validation but is now failing on insertion).
There are a lot of reasons to use square bracket notation, obviously. Dot notation has tons of limitations, despite being more readable.
2. Looking at the example above defining the schema, the keys in the schema have to be the full path in the object. You have to declare name[first_name] to assign a schema to it, which means that you're not only defining the properties of first_name, but you're also defining the structure of the object. first_name has to be nested inside name.
You can't, for example, just do
Schemas.NamesCollection = new SimpleSchema({
name: {
type: Object
},
first_name : {
type: String,
optional: false,
label: "First Name"
},
last_name : {
type: String,
optional: false,
label: "Last Name"
}
});
And then proceed to store first_name and last_name inside of name, can you?

Resources