In Prod

← Back to Posts

Type safe updates using type inference

February 19, 2020

A common problem we see when creating forms in React is the need for a generalised method that will update the form values when an input changes.

For example, we might need to change the following object:

FormObject.ts

_8
export class FormObject {
_8
public Name: string;
_8
public DateOfBirth: Date;
_8
public Age: number;
_8
// depending on ts strictness, we may need to specify an array accessor
_8
// property for this to work in the first method
_8
[key: string]: string | Date | number;
_8
}

When using input fields or components in react, we can pass the value of the input to an update method and specify the property to update:

index.tsx

_12
// method that updates our object by accepting the name of the property to update
_12
// and the value of a type of the property
_12
function updateObject(name: string, value: string | Date | number) {
_12
formObject[name] = value;
_12
setObject(formObject);
_12
}
_12
_12
// input that calls that update method on change
_12
<GenericTextInput
_12
value={formObject.Name}
_12
onChange={(value: string) => updateObject("Name", value)}
_12
/>

This works but it isn't type safe. This means that we could pass a value that is not appropriate for the property, or even try to update a property that does not exist on the object (spelling mistake or otherwise):

index.tsx

_12
<GenericTextInput
_12
value={formObject.Name}
_12
// the Nam property does not exist but the method will happily accept it
_12
onChange={(value: string) => updateObject("Nam", value)}
_12
/>
_12
_12
<GenericTextInput
_12
value={formObject.Name}
_12
// the Age property is expecting a number but will receive a string,
_12
// which may cause errors down the line
_12
onChange={(value: string) => updateObject("Age", value)}
_12
/>


Index Typing

To help with this problem we can leverage some advanced typescript features that allow us to construct a generic method that will require us to specify only values of properties that exist for a type, as well as the correct types of values to assign to those properties:

helpers.ts

_11
/**
_11
* Update an object with type safety for both keys and values
_11
* @param source any object T
_11
* @param key any key of object T to update
_11
* @param value value to update the property K to
_11
* @return updated object T
_11
*/
_11
export function ChangeObjectValue<T, K extends keyof T>(source: T, key: K, value: T[K]) {
_11
source[key] = value;
_11
return { ...source };
_11
}

In the above method we are using a generic type T as the source object, a key of this type K and the value to assign it which is then required to be of index type T[K]. The method is then used like this:

index.tsx

_1
let updatedObj = ChangeObjectValue(formObject, "DateOfBirth", new Date());

T becomes type FormObject. The key parameter will then only accept valid properties of T, such as DateOfBirth in this case. The value parameter will only accept the correct type for the key, which in this case is a Date. Our method returns a 'spread' of the object which creates a new instance, this helps play nice with set hooks.

We can then use this generic method in our input components and can wrap it in a set function if needed:

index.tsx

_12
<GenericTextInput
_12
value={formObject.Name}
_12
onChange={(value: string) => setObject(ChangeObjectValue(formObject, "Name", value))}
_12
/>
_12
<GenericDatePicker
_12
value={formObject.DateOfBirth}
_12
onChange={(value: Date) => setObject(ChangeObjectValue(formObject, "DateOfBirth", value))}
_12
/>
_12
<GenericNumberField
_12
value={formObject.Age}
_12
onChange={(value: number) => setObject(ChangeObjectValue(formObject, "Age", value))}
_12
/>


Mapped and Conditional Typing

We can expend this concept to make our method only allow us to update fields of a certain type. For example, if we had a user picker that returned a string Id and multiple user fields on an object, we only want to be able to update those user fields from this component.

To do this we leverage mapped and conditional types in typescript:

helpers.ts

_6
/**
_6
* @param T Type to ensure keys on
_6
* @param V Value of type to ensure
_6
* @return New generic type
_6
*/
_6
export type KeysMatching<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T];

Here we have created a new type that requires an object T and a second parameter as the type that we want to specify, V. This returns a set of properties that use K in T.

The type definition takes the generic objects keys K and then looks for types that match the type V , from which it conditionally returns either the property of the specified type or never. It then filters this list to only return valid properties (i.e. removes all never entries).

We can then use this type in a generic method to only allow mutation of the specified properties:

MyObject.ts

_6
export class MyObject {
_6
public Name: string;
_6
public Age: number;
_6
public Mother: User;
_6
public Father: User;
_6
}

index.tsx

_10
// we consume the new type definition by using it as our key type and
_10
// specifying our generic object, and the type we wish to filter to
_10
// the value parameter in this case can be any type (in this case string) to
_10
// be used to get the correct type for the key
_10
function changeUserValue(key: KeysMatching<MyObject, User>, value: string) {
_10
let user = users.filter(item => item.id === value)[0];
_10
// once we have our correct value we can use our type inference method from above
_10
// to safely update our User property
_10
ChangeObjectValue(myObj, key, user);
_10
}

Using the method

The above method only allows changes to properties that are of type User and only accepts string values. In our user picker example the usage might be something like:

index.tsx

_5
// available key values on this method would be ["Mother", "Father"]
_5
<UserPicker
_5
selectedId={myObj.Mother.Id}
_5
onChange={(value: string) => changeUserValue("Mother", value)}
_5
/>

The fact that our type safe method only allows us to update specific User fields on an object means we end up with fewer mistakes, that in some cases, can be quite difficult to debug.


Andrew McMahon
These are a few of my insignificant productions
by Andrew McMahon.