how to represent state object as typescript interface - reactjs

I thought I am comfortable with Javascript and React, but currently suffering through typescript learning curve. I have my react state defined as:
state = {
fields: { // list of fields
symbol: '',
qty: '',
side: ''
},
fieldErrors: {}
};
I want to be able to use it as following (dictionary):
onInputChange = (name :string, value :string, error :string) => {
const fields = this.state.fields;
const fieldErrors = this.state.fieldErrors;
fields[name] = value;
fieldErrors[name] = error;
this.setState({fields, fieldErrors});
}
How do I represent my state in terms of Typescript? I am trying something like:
interface IFields {
name: string
}
interface IOrderEntryState {
fields: IFields,
fieldErrors: IFields
}
Pardon if my question sounds illiterate, totally new at this. Thanks

Based on your snippet, it looks like fields[name] is assigning an arbitrary key to that object. So you probably want to use an index signature to represent that instead of the hardcoded key name as you have now.
So your interfaces probably should look more like this:
interface IFields {
// This is an index signature. It means this object can hold
// a key of any name, and they can be accessed and set using
// bracket notation: `this.state.fields["somekey"]`
[name: string]: string
}
interface IOrderEntryState {
fields: IFields,
fieldErrors: IFields
}

If you want a dictionary you can declare one using generic type with index property. It would look like this:
interface Dictionary<TKey, TVal> {
[key: TKey]: TVal;
}
interface IOrderEntryState {
fields: Dictionary<string, string>,
fieldErrors: Dictionary<string, string>
}
This makes IOrderEntryState.fields have arbitrary string attribute names with string values.

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

TypeScript Objects as Dictionary types as in C#

I have some JavaScript code that uses objects as dictionaries; for example a 'person' object will hold a some personal details keyed off the email address.
var people = {<email> : <'some personal data'>};
adding > "people[<email>] = <data>;"
getting > "var data = people[<email>];"
deleting > "delete people[<email>];"
Is it possible to describe this in Typescript? or do I have to use an Array?
In newer versions of typescript you can use:
type Customers = Record<string, Customer>
In older versions you can use:
var map: { [email: string]: Customer; } = { };
map['foo#gmail.com'] = new Customer(); // OK
map[14] = new Customer(); // Not OK, 14 is not a string
map['bar#hotmail.com'] = 'x'; // Not OK, 'x' is not a customer
You can also make an interface if you don't want to type that whole type annotation out every time:
interface StringToCustomerMap {
[email: string]: Customer;
}
var map: StringToCustomerMap = { };
// Equivalent to first line of above
In addition to using an map-like object, there has been an actual Map object for some time now, which is available in TypeScript when compiling to ES6, or when using a polyfill with the ES6 type-definitions:
let people = new Map<string, Person>();
It supports the same functionality as Object, and more, with a slightly different syntax:
// Adding an item (a key-value pair):
people.set("John", { firstName: "John", lastName: "Doe" });
// Checking for the presence of a key:
people.has("John"); // true
// Retrieving a value by a key:
people.get("John").lastName; // "Doe"
// Deleting an item by a key:
people.delete("John");
This alone has several advantages over using a map-like object, such as:
Support for non-string based keys, e.g. numbers or objects, neither of which are supported by Object (no, Object does not support numbers, it converts them to strings)
Less room for errors when not using --noImplicitAny, as a Map always has a key type and a value type, whereas an object might not have an index-signature
The functionality of adding/removing items (key-value pairs) is optimized for the task, unlike creating properties on an Object
Additionally, a Map object provides a more powerful and elegant API for common tasks, most of which are not available through simple Objects without hacking together helper functions (although some of these require a full ES6 iterator/iterable polyfill for ES5 targets or below):
// Iterate over Map entries:
people.forEach((person, key) => ...);
// Clear the Map:
people.clear();
// Get Map size:
people.size;
// Extract keys into array (in insertion order):
let keys = Array.from(people.keys());
// Extract values into array (in insertion order):
let values = Array.from(people.values());
You can use templated interfaces like this:
interface Map<T> {
[K: string]: T;
}
let dict: Map<number> = {};
dict["one"] = 1;
You can use Record for this:
https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkt
Example (A mapping between AppointmentStatus enum and some meta data):
const iconMapping: Record<AppointmentStatus, Icon> = {
[AppointmentStatus.Failed]: { Name: 'calendar times', Color: 'red' },
[AppointmentStatus.Canceled]: { Name: 'calendar times outline', Color: 'red' },
[AppointmentStatus.Confirmed]: { Name: 'calendar check outline', Color: 'green' },
[AppointmentStatus.Requested]: { Name: 'calendar alternate outline', Color: 'orange' },
[AppointmentStatus.None]: { Name: 'calendar outline', Color: 'blue' }
}
Now with interface as value:
interface Icon {
Name: string
Color: string
}
Usage:
const icon: SemanticIcon = iconMapping[appointment.Status]
You can also use the Record type in typescript :
export interface nameInterface {
propName : Record<string, otherComplexInterface>
}
Lodash has a simple Dictionary implementation and has good TypeScript support
Install Lodash:
npm install lodash #types/lodash --save
Import and usage:
import { Dictionary } from "lodash";
let properties : Dictionary<string> = {
"key": "value"
}
console.log(properties["key"])

Resources