Chapters - Table of contents ā
You will find below a summary of all the essential knowledge on re-usable components, functions and, types to build professional applications.
Function overloads
- Each overload define a specific use-case
- The function implementation should cover all cases (all overloads definitions)
1interface RefObject {
2 readonly current: any
3}
4
5interface MutableRefObject {
6 current: any
7}
8
9interface UseRef {
10 (initialValue: any): RefObject;
11 (): MutableRefObject;
12}
13
14// the implementation must accept none or `any` type of `initialValue`
15// depending on the scenario, the function can return either a
16// - a `RefObject`
17// or
18// - a `MutableRefObject`
19const useRef: UseRef = (ref?: any): MutableRefObject | RefObject => {
20 return {
21 current: null
22 }
23}
24
ā¹ļø When to use Function overloads?
- Always try to type a function with union types first
- If it's not possible, move forward with function overloads.
Generics
- Leveraging Generics allows building more flexible functions.
1function customDateSort<T extends { createdAt: Date, updatedAt?: Date }>(
2 items: T[]
3) {
4 return items.sort(
5 (a, b) => compareDesc(
6 (a.updatedAt || a.createdAt),
7 (b.updatedAt || b.createdAt)
8 )
9 )
10}
11
12// we added constraints (requiring an array of items with a `createdAt` property)
13// while keeping the input flexilbe, allowing other properties (`title`)
14const sortedList = customDateSort([
15 { createdAt: new Date(), title: "Blog post 1" },
16]);
17
- A function can have multiple type arguments
- A type argument can have a default value
- We can add a constraint to a type argument by adding a extends.
A type argument value can be inferred from a function argument's type
1// `T` is a type argument, that, if not provided
2// will be assigned with the type inferred from `initialValue`
3function useState<T>(initialValue: T) : [
4 value: T,
5 setter: (value: T) => void
6] {
7// ...
8}
9
10// T = Person
11const [value] = useState<Person>({ name: 'John', position: undefined })
12
13// T = { name: string; position: undefined; }
14const [value] = useState({ name: 'John', position: undefined })
15
Manipulate types: type, type helpers
A good practice to keep types strong and maintainable at scale is to avoid declarations duplication and build types on top of each other.
TypeScript is providing a set of type helpers to extract sub-parts or transform existing types:
1import { ButtonProps } from '@material-ui/core'
2
3type UIKitButton = {
4 onClick: Promise<() => boolean> | () => void
5} & Omit<ButtonProps, 'loading' | 'onClick'>
6
Other TypeScript type helpers to keep in mind š”
-
Pick<T, K>: given a T type and a K type, return a new type only including the K properties of T
Pick<{ a: string, b: number }, 'a'>gives{ a: string }
-
Required<T> is the opposite of Partial<T>
Required<{ a?: string, b?: number }, 'a'>gives
{ a: string, b: number }
-
NonNullable<T> removes null and undefined from a givenTtype
NonNullable<string | null> gives string
-
ReturnType gives the return type of the provided function type
ReturnType<(a: number, b number) => number> gives number
When should I use interface or type? A rule of thumb š
You will need type to achieve the following:
-
Rename an existing type
type Name = string
-
Transform an existing type (changing properties type)
type RegisterUserForm = Partial<Omit<User, 'id'>>
-
Create a subset of a type
type UserWithId = Pick<User, 'id'>
-
Combine multiple types together
type E = A & B & C
ā”ļø Otherwise, you can use the interface keyword.
Advanced types: Mapped types
All previously seen _type helpers _are heavily relying on TypeScript Mapped types to transform existing types.
Mapped types are based on the _index signature _syntax of TypeScript, used to define more dynamic types by specifying the expected index signature of an iterable/object:
1interface ButtonProps {
2 name: string,
3 // ...
4
5 // index signature
6 [k: string]: string
7}
8
9// `ButtonProps` is now an "open object",
10// accepting dynamic properties having a string name and value:
11const buttonProps: ButtonProps = {
12 name: 'Bob',
13 city: 'Paris',
14}
15
- brackets are surrounding the key type, here k
- the key signature [k: string] is prefixed with a type: string
In short, { [k: string]: string } describes:
An object that can accept any property with a key of type string and value of type string
Mapped types, leveraging index signature syntax, allows to "iterates" over a given type argument in order to transform it:
1// Given a type `Type`, returns all its properties as boolean
2interface ToBoolean<Type> {
3 [Property in keyof Type]: boolean
4}
5
6type UserConfig = ToBoolean<{ acceptedPolicy: string, signedIn: string }>
7// UserConfig type is
8// {
9// acceptedPolicy: boolean
10// signedIn: boolean
11// }
12