trivago tech tips - #22 Typescript Edition
At trivago we are using TypeScript in several frontend projects. It helps our developers be more productive through features like autocompletion, type checking and source documentation. Here are our five top hacks for using Typescript based on our experience!
Tip #1 Use generics to create reusable type-safe functions, classes and interfaces.
Generics allow you to create reusable type-safe functions, classes and interfaces.
Let's say you have a type Hotel
and a type Activity
that are used throughout your app and often come in arrays. In order to remove a certain Hotel
from a Hotel
array, or Activity
from an Activity
array you would need to implement two separate functions, that can deal with either the Hotel
or the Activity
type:
// Can only deal with objects of type `Hotel`
const removeFromHotelArray = (hotels: Hotel[], hotel: Hotel) =>
hotels.filter((i) => i !== hotel);
// Can only deal with objects of type `Activity`
const removeFromActivityArray = (activities: Activity[], activity: Activity) =>
activities.filter((i) => i !== activity);
// Can deal with any array type
const remove = <T>(array: T[], item: T) => array.filter((i) => i !== item);
This is where Generics come in handy. Instead of defining a certain type you can add a "type variable" (Generic) that will take the shape of whatever type you pass to it.
Let's have a look at the remove
function. In order to allow such a type variable, you can add one or multiple type variables just before your function signature: <T>
This variable can now be used within your function signature and body. This way we can now pass Hotel
types, Activity
types or whatever type your array comes in.
But why not use any
? In the example above the output array will have the same type as the input array, while when using any,
your output would be of type any[]
.
See more example code here.
Tip #2 Use ReturnType
to get the signature of a function's return value
The ReturnType
utility lets you get the type of a function’s return value. This can come in handy when a function’s outcome changes often. Instead of defining a static type and updating it with every change, just let TS infer the return type of your function and use the ReturnType
utility to get that signature.
const getHotelInfo = (
id: number) => ({
name: '25 hours hotel',
rating: 7500
});
// `logHotelInfo` now expects an object that has the same signature as
// those objects returned from `getHotelInfo`
const logHotelInfo = (info: ReturnType<typeof getHotelInfo>) => {
console.log(info);
};
logHotelInfo(getHotelInfo(1337));
Access the code here.
Tip #3 Create your own type guards to narrow down the type of a value
Let's say you have a type Hotel
and a type Apartment
that both are extending the Accommodation
type. In order to check if an accommodation is of type Hotel
you can create a function that uses a type predicate
to tell TypeScript that if your function returns true
the accommodation is of type Hotel. In order to do so, just add the type predicate as
return value to your function like: const isHotel = (acc: Accommodation): acc is Hotel => { /* ... */ }
. You can now use this function e.g. in an if
statement to narrow down the type:
interface Accommodation {
type: string;
id: number;
}
interface Hotel extends Accommodation {
type: 'hotel';
stars: number;
}
interface Apartment extends Accommodation {
type: 'apartment';
isEntirePlace: boolean;
}
const isHotel = (accommodation: Accommodation): accommodation is Hotel =>
accommodation.type === 'hotel';
const accommodation = {
type: 'hotel',
id: 434211,
stars: 4
};
if (isHotel(accommodation)) {
console.log(accommodation.stars);
}
Access the code here.
Tip #4 Use Mapped Types to automatically transform interfaces
Let’s say we have an interface Parameters
that defines a set of properties that you use throughout your app. Some modules though expect a stringified version of those properties. Instead of defining a whole new interface with the same properties as strings, we can automatically let the interface be transformed so that all properties are defined as strings. Have a look at the Stringify
type in the example below. It expects an input type T
and maps all its properties to a string type. This way we can pass our Parameters
interface and get a StringifiedParameters
interface.
export type Stringify<T> = { [P in keyof T]: string };
type StringifiedParamters = Stringify<Parameters>;
interface Parameters {
maxPrice: number;
arrival: Date
departure: Date;
}
const params: Parameters = {
maxPrice: 99,
arrival: new Date(2021, 10, 1),
departure: new Date(2021, 10, 8)
};
const stringified: StringifiedParamters = {
maxPrice: '99',
arrival: '2021-10-01',
departure: '2021-10-08'
};
Tip #5 Using tuple types can simplify returning multiple values from a function in a predictable way
In the TypeScript world, a tuple is a special type of array. It has a fixed sized and each position in the array has a specific type. Most commonly as TypeScript developers we see this in the context of React. In React, the useState
hook returns us an array of two things: the state and the function we use to set the state. Because of how we declare tuples, TypeScript will tell us when we use the wrong type in the wrong position, as well as when we add or remove something on an index that’s out of bounds.
Suppose we’re building a React hook that derives its state from the URL. We want to return that derived state, but we also want to return a function that adds new data to the URL. Our tuple would look something like this:
type SearchState = {
maxPrice: number;
arrival: Date;
departure: Date;
}
type SetSearchState = (nextState: Partial<SearchState>) => void;
// this guarantees that our return value will be an array with
// exactly 2 items, the first of which is the search state, and
// the second the setter for the search state
type SearchStateTuple = [SearchState, SetSearchState];
function useSearchState(): SearchStateTuple {
const state = deriveStateFromURL();
const setState = (nextState) => {
const newState = merge(state, nextState);
updateURL(newState);
};
return [state, setState];
}
One of the main reason to do something like this is to simplify the renaming of variables for when you use a function multiple times in a row. Again, most notably this happens with the useState
hook in React. This
const [ toggleState, setToggleState ] = useState<boolean>(false);
is far easier to read and understand, than
const { state: toggleState, setState: setToggleState } = useState<boolean>(false);
especially when you have multiple useState calls in a row:
const [ count, setCount ] = useState<number>(0);
const [ toggle, setToggle ] = useState<boolean>(false);
const [ query, setQuery ] = useState<string>('');
Access the code here.
That’s a wrap on our Typescript tips for the week! Do you have any to add? Let us know below.