How to use interface to type function argument in Flow - reactjs

I'm trying to implement a React component that contains a list of options and shows their id and name. I want this component to be reusable so I define an interface Option to ensure the required fields are always provided.
And here comes the issue: if I pass any type with more fields than those 2 { id, name, /* anything */}, Flow complains. Is it not possible to use interfaces in Flow like this?
Here's the minimal relevant code:
interface Option {
id: string,
name: string
}
const List = (options: Option[]) => {
options.forEach(o => null)
}
type ImplementsOption = {
id: string,
name: string,
description: string
}
const plans: ImplementsOption[] = []
List(plans)
Error:
Cannot call List with plans bound to options because property description is missing in Option 1 but exists in ImplementsOption [2] in array element.
Trying with casting:
List((plans: Option[]))
And also with classes:
class ComplexOption implements Option {
id: string
name: string
}
const complexOptions: ComplexOption[] = []
List(complexOptions)
Nothing seems to work!
There is a playground with all these snippets already.

Imagine we had a list of ImplementsOption: [{ id: 'id', name: 'name', description: 'description' }, ...]. Now we pass it into the List function, which has the signature Option[] => void. This is totally valid from the point of view of List since ImplementOption is a supertype of Option. However, there is no guarantee in the List function that it won't modify the list that is passed in. Thus, the function could add an element of type Option to the list, which would be valid for a Option[] but invalid for a ImplementsOption[].
To fix this, you can type plans as a $ReadOnlyArray<Option>, which tells Flow that the List function will not modify the elements (try flow).
Reference issues #4425, #4483, or #5206 for more information.

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

How do you write flexible typescript types/interfaces for client-side documents?

Let's say we have the following models:
const catSchema = new Schema({
name: String,
favoriteFood: { type: Schema.Types.ObjectId, ref: 'FoodType' },
});
const foodType = new Schema({
name: String,
});
Here we can see that favoriteFood on the catSchema is a reference to another collection in our database.
Now, let's say that the getAllCats api does not populate the favoriteFood field because it isn't necessary and therefor just returns the reference id for foodType. The response from the api might look like this:
[
{name: 'Fluffy', favoriteFood: '621001113833bd74d6f1fc8c'},
{name: 'Meowzer', favoriteFood: '621001113833bd74d6f1fc4b'}
]
However, the getOneCat api DOES populate the favoriteFood field with the corresponding document. It might look like this:
{
name: 'Fluffy',
favoriteFood: {
name: 'pizza'
}
}
My question, how does one write a client side interface/type for my cat document?
Do we do this?
interface IFavoriteFood {
name: string
}
interface ICat {
name: string,
favoriteFood: IFavoriteFood | string
}
Say we have a React functional component like this:
const Cat = (cat: ICat) => {
return (
<div>
${cat.favoriteFood.name}
</div>
)
}
We will get the following typescript error :
"Property 'name' does not exist on type 'string | IFavoriteFood'.
Property 'name' does not exist on type 'string'."
So, I have to do something like this to make typescript happy:
const Cat = (cat: ICat) => {
return (
<div>
${typeof cat.favoriteFood === 'string' ? 'favoriteFood is not populated': cat.favoriteFood.name}
</div>
)
}
Do I write two separate interfaces? One for the cat object with favoriteFood as a string for the objectId and one for cat with favoriteFood as the populated object?
interface IFavoriteFood {
name: string
}
interface ICat {
name: string,
favoriteFood: string
}
interface ICatWithFavoriteFood {
name: string,
favoriteFood: IFavoriteFood
}
const Cat = (cat: ICatWithFavoriteFood) => {
return (
<div>
${cat.favoriteFood.name}
</div>
)
}
Would love to hear how people approach this in their codebase. Also open to being pointed to any articles/resources that address this issue.
This:
favoriteFood: IFavoriteFood | string
Is a bad idea and will lead to a lot of ugly code trying to sort out when it's one data type versus the other.
I think a better approach (and one I personally use a lot) would be:
favoriteFoodId: string
favoriteFood?: IFavoriteFood
So favoriteFoodId is always there, and is always a string. And a full favoriteFood object is sometimes there.
Now to use that value is a very simple and standard null check.
const foodName = cat.favoriteFood?.name ?? '- ice cream, probably -'
Note, this does mean changing you schema a bit so that foreign keys are suffixed with Id to not clash with the keys that will contain the actual full association data.
You could extend this to the two interface approach as well, if you wanted to lock things down a bit tighter:
interface ICat {
name: string,
favoriteFoodId: string
favoriteFood?: null // must be omitted, undefined, or null
}
interface ICatWithFavoriteFood extends ICat {
favoriteFood: IFavoriteFood // required
}
But that's probably not necessary since handling nulls in your react component is usually cheap and easy.

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.

Define type based on value of keys in an object

I am adding one Select component that has the following structure.
type Option = {
value: string | number
label: string;
icon?: string
}
type SelectProps = {
labelKey?: string;
iconKey?: string;
valueKe?: string;
options: Option[]
}
function Select({
labelKey = 'label',
iconKey = 'icon',
valueKey= 'value',
groupBy = 'groupBy',
options// need to type Option
}: SelectProps) {
// some logic to render options
// options.map(option => (<li key={option[valueKey]}>{option[labelKey]}</li>))
}
Here, options is an array of options and I am trying to give flexibility to users to provide keys to use for labels, icons etc so the user doesn't need to map data all the time.
For now, the Option type has hardcode keys like label, value, icon but I want to create this type based on values passed to labelKey, valueKey, iconKey etc.
For example, if a user passes the labelKey="name" prop then the Option type should allow the following data:
[ {
name: 'Product',
value: 'product',
icon: 'product'
}]
So far I have tried the following implementation but it sets all keys' types to string.
type OptionKeys = {labelKey: string, valueKey: string, iconKey: string}
type Option<T extends OptionKeys> = {
[label in T["labelKey" | "valueKey" | "iconKey"]]: string // all the types are string
}
type SelectProps<T extends OptionKeys = {
labelKey: 'label',
valueKey: 'value',
iconKey: 'icon'
}> = {
labelKey?: string
valueKey?: string;
iconKey?: string;
options: Option<T>[]
}
Here, the Option's keys have value of type string but I want to define type based on key. For example, if the key is labelKey, I want its value to be number | string etc.
One option, I see here is to accept OptionType from outside by making the Select component generic but in that case, I need to refactor my component and want to avoid this refactoring at the moment.
How can I update my type to handle this scenario?
Using the FromEntries type from this blog post: https://dev.to/svehla/typescript-object-fromentries-389c,
Define the SelectProps type as follows:
type SelectProps<LK, VK, IK> = {
labelKey?: LK
valueKey?: VK
iconKey?: IK
options: FromEntries<[
[LK, string],
[VK, string | number],
[IK, string | undefined]
]>
}
We have three keys in the options, the label key (LK) which is a string, the value key (VK) which is a string | number and the icon key (IK) which is a string | undefined
Now we define the Select function as follows:
function Select<
LK extends string = 'label',
VK extends string = 'value',
IK extends string = 'icon'
>(props: SelectProps<LK, VK, IK>) {}
It is important to put the default key names on the function itself rather than the SelectProps type. I am not sure why.
Full playground link

How to pass root parameters in the resolver function of a nested query?

I have a query of the following nature
Category1(name: $cat1){
Category2(secondName: $cat2){
secondName
}}
My schema is like so:
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
Category1: {
type: new GraphQLList(Category1Type),
args: { name },
resolve: resolveCategory1
}}
})
And then the Category1Type is defined as:
const Category1Type = new GraphQLObjectType({
name: 'Category1',
description: '<>',
fields: () => ({
name: { type: GraphQLString },
category2: {
type: new GraphQLList(CategoryType2),
args: { secondName },
resolve: resolveCategory2
}
})
});
For simplicity sake, assume category2 is like so:
const Category2Type = new GraphQLObjectType({
name: 'Category2',
description: '<>',
fields: () => ({
name: { type: GraphQLString },
})
});
Now I want to fetch all Category2 items under Category1 with option to filter, like so:
Category1(name: $name){
name
category2(name: $name){
name
}}
My resolvers are defined like so:
# Category1 resolver
function cat1resolve (root, args) {
return SELECT * from data WHERE category1_name = args.name
}
# Category2 resolver
function cat2Resolve (root, args) {
return SELECT * from data WHERE category1_name = rootargs.name and categort2_name = args.secondName }
Now the problem is that the 'resolver' for cat2Resolve is not able to see or receive the rootargs.name for me to do this kind of filtering.
The resolve function signature includes 4 parameters. From Apollo's docs:
obj: The object that contains the result returned from the resolver on the parent field, or, in the case of a top-level Query field, the
rootValue passed from the server configuration. This argument enables
the nested nature of GraphQL queries.
args: An object with the arguments passed into the field in the query. For example, if the field was called with author(name: "Ada"),
the args object would be: { "name": "Ada" }.
context: This is an object shared by all resolvers in a particular query, and is used to contain per-request state, including
authentication information, dataloader instances, and anything else
that should be taken into account when resolving the query. If you’re
using Apollo Server, read about how to set the context in the setup
documentation.
info: This argument should only be used in advanced cases, but it contains information about the execution state of the query, including
the field name, path to the field from the root, and more. It’s only
documented in the GraphQL.js source code.
Note: These docs are for graphql-tools' makeExecutableSchema (which I highly recommend) but the same applies to plain old GraphQL.JS.
The key point here is that a resolver for a particular field is generally agnostic to what other resolvers do or what information is passed to them. It's handed its own parent field value, its own arguments, the context and expected to work with that.
However, there is a workaround utilizing the info parameter. The object passed to info is huge and can be complicated to parse, but contains virtually all the information about the requested query itself. There are libraries out to help with parsing it, but you may want to print the whole thing to console and poke around (it's pretty cool!).
Using something like lodash's get, we can then do:
const category1id = get(info, 'operation.selectionSet.selections[0].arguments[0].value.value')
and utilize that value inside your query. The above is pretty fragile, since it assumes your request only contains the one query, and you only have one argument on the Category1 field. In practice, you'd probably want to utilize Array.find and look up the fields/arguments by name, but this should give you a starting point.

Resources