Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[No QA] Add TypeScript Guideline #21549

Merged
merged 20 commits into from
Jul 24, 2023
142 changes: 142 additions & 0 deletions contributingGuides/PROPTYPES_CONVERSION_TABLE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Expensify PropTypes Conversion Table

## Table of Contents

- [Important Considerations](#important-considerations)
- [Don't Rely on `isRequired`](#dont-rely-on-isrequired)
- [PropTypes Conversion Table](#proptypes-conversion-table)
- [Conversion Example](#conversion-example)

## Important Considerations

### Don't Rely on `isRequired`

Regardless of `isRequired` is present or not on props in `PropTypes`, read through the component implementation to check if props without `isRequired` can actually be optional. The use of `isRequired` is not consistent in the current codebase. Just because `isRequired` is not present, it does not necessarily mean that the prop is optional.

One trick is to mark the prop in question with optional modifier `?`. See if the "possibly `undefined`" error is raised by TypeScript. If any error is raised, the implementation assumes the prop not to be optional.

```ts
// Before
const propTypes = {
isVisible: PropTypes.bool.isRequired,
// `confirmText` prop is not marked as required here, theoretically it is optional.
confirmText: PropTypes.string,
};

// After
type Props = {
isVisible: boolean;
// Consider it as required unless you have proof that it is indeed an optional prop.
confirmText: string; // vs. confirmText?: string;
};
```

## PropTypes Conversion Table

| PropTypes | TypeScript | Instructions |
| -------------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PropTypes.any` | `T`, `Record<string, unknown>` or `unknown` | Figure out what would be the correct data type and use it.<br><br>If you know that it's a object but isn't possible to determine the internal structure, use `Record<string, unknown>`. |
| `PropTypes.array` or `PropTypes.arrayOf(T)` | `T[]` or `Array<T>` | Convert to `T[]`, where `T` is the data type of the array.<br><br>If `T` isn't a primitive type, create a separate `type` for the object structure of your prop and use it. |
| `PropTypes.bool` | `boolean` | Convert to `boolean`. |
| `PropTypes.func` | `(arg1: Type1, arg2: Type2...) => ReturnType` | Convert to the function signature. |
| `PropTypes.number` | `number` | Convert to `number`. |
| `PropTypes.object`, `PropTypes.shape(T)` or `PropTypes.exact(T)` | `T` | If `T` isn't a primitive type, create a separate `type` for the `T` object structure of your prop and use it.<br><br>If you want an object but isn't possible to determine the internal structure, use `Record<string, unknown>`. |
hayata-suenaga marked this conversation as resolved.
Show resolved Hide resolved
| `PropTypes.objectOf(T)` | `Record<string, T>` | Convert to a `Record<string, T>` where `T` is the data type of values stored in the object.<br><br>If `T` isn't a primitive type, create a separate `type` for the object structure and use it. |
| `PropTypes.string` | `string` | Convert to `string`. |
| `PropTypes.node` | `React.ReactNode` | Convert to `React.ReactNode`. `ReactNode` includes `ReactElement` as well as other types such as `strings`, `numbers`, `arrays` of the same, `null`, and `undefined` In other words, anything that can be rendered in React is a `ReactNode`. |
| `PropTypes.element` | `React.ReactElement` | Convert to `React.ReactElement`. |
| `PropTypes.symbol` | `symbol` | Convert to `symbol`. |
| `PropTypes.elementType` | `React.ElementType` | Convert to `React.ElementType`. |
| `PropTypes.instanceOf(T)` | `T` | Convert to `T`. |
| `PropTypes.oneOf([T, U, ...])` or `PropTypes.oneOfType([T, U, ...])` | `T \| U \| ...` | Convert to a union type e.g. `T \| U \| ...`. |

## Conversion Example

```ts
// Before
const propTypes = {
unknownData: PropTypes.any,
anotherUnknownData: PropTypes.any,
indexes: PropTypes.arrayOf(PropTypes.number),
items: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.string,
label: PropTypes.string,
})
),
shouldShowIcon: PropTypes.bool,
onChangeText: PropTypes.func,
count: PropTypes.number,
session: PropTypes.shape({
authToken: PropTypes.string,
accountID: PropTypes.number,
}),
errors: PropTypes.objectOf(PropTypes.string),
inputs: PropTypes.objectOf(
PropTypes.shape({
id: PropTypes.string,
label: PropTypes.string,
})
),
label: PropTypes.string,
anchor: PropTypes.node,
footer: PropTypes.element,
uniqSymbol: PropTypes.symbol,
icon: PropTypes.elementType,
date: PropTypes.instanceOf(Date),
size: PropTypes.oneOf(["small", "medium", "large"]),

optionalString: PropTypes.string,
/**
* Note that all props listed above are technically optional because they lack the `isRequired` attribute.
* However, in most cases, props are actually required but the `isRequired` attribute is left out by mistake.
*
* For each prop that appears to be optional, determine whether the component implementation assumes that
* the prop has a value (making it non-optional) or not. Only those props that are truly optional should be
* labeled with a `?` in their type definition.
*/
};

// After
type Item = {
value: string;
label: string;
};

type Session = {
authToken: string;
accountID: number;
hayata-suenaga marked this conversation as resolved.
Show resolved Hide resolved
};

type Input = {
id: string;
label: string;
};

type Size = "small" | "medium" | "large";

type Props = {
unknownData: string[];

// It's not possible to infer the data as it can be anything because of reasons X, Y and Z.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
anotherUnknownData: unknown;

indexes: number[];
items: Item[];
shouldShowIcon: boolean;
onChangeText: (value: string) => void;
count: number;
session: Session;
errors: Record<string, string>;
inputs: Record<string, Input>;
label: string;
anchor: React.ReactNode;
footer: React.ReactElement;
uniqSymbol: symbol;
icon: React.ElementType;
date: Date;
size: Size;
optionalString?: string;
};
```
236 changes: 236 additions & 0 deletions contributingGuides/TS_CHEATSHEET.md
hayata-suenaga marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# Expensify TypeScript React Native CheatSheet

## Table of Contents

- [CheatSheet](#cheatsheet)
- [1.1 `props.children`](#children-prop)
- [1.2 `forwardRef`](#forwardRef)
- [1.3 Style Props](#style-props)
- [1.4 Animated styles](#animated-style)
- [1.5 Render Prop](#render-prop)
- [1.6 Type Narrowing](#type-narrowing)
- [1.7 Errors in Try-Catch Clauses](#try-catch-clauses)
- [1.8 Const Assertion](#const-assertion)
- [1.9 Higher Order Components](#higher-order-components)
- [1.10 Function Overloading](#function-overloading)

## CheatSheet
hayata-suenaga marked this conversation as resolved.
Show resolved Hide resolved

<a name="children-prop"></a><a name="1.1"></a>

- [1.1](#children-prop) **`props.children`**

```tsx
type WrapperComponentProps = {
children?: React.ReactNode;
};

function WrapperComponent({ children }: WrapperComponentProps) {
return <View>{children}</View>;
}

function App() {
return (
<WrapperComponent>
<View />
</WrapperComponent>
);
}
```

<a name="forwardRef"></a><a name="1.2"></a>

- [1.2](#forwardRef) **`forwardRef`**

```ts
import { forwardRef, useRef, ReactNode } from "react";
import { TextInput, View } from "react-native";

export type CustomTextInputProps = {
label: string;
children?: ReactNode;
};

const CustomTextInput = forwardRef<TextInput, CustomTextInputProps>(
(props, ref) => {
return (
<View>
<TextInput ref={ref} />
{props.children}
</View>
);
}
);

function ParentComponent() {
const ref = useRef<TextInput>();
return <CustomTextInput ref={ref} label="Press me" />;
}
```

<a name="style-props"></a><a name="1.3"></a>

- [1.3](#style-props) **Style Props**

Use `StyleProp<T>` to type style props. For pass-through style props, use types exported from `react-native` for the type parameter (e.g. `ViewStyle`).

```tsx
import { StyleProp, ViewStyle, TextStyle, ImageStyle } from "react-native";

type MyComponentProps = {
containerStyle?: StyleProp<ViewStyle>;
textStyle?: StyleProp<TextStyle>;
imageStyle?: StyleProp<ImageStyle>;
};

function MyComponent({ containerStyle, textStyle, imageStyle }: MyComponentProps) = {
<View style={containerStyle}>
<Text style={textStyle}>Sample Image</Text>
<Image style={imageStyle} src={'https://sample.com/image.png'} />
</View>
}
```

<a name="animated-style"></a><a name="1.4"></a>

- [1.4](#animated-style) **Animated styles**

```ts
import {useRef} from 'react';
import {Animated, StyleProp, ViewStyle} from 'react-native';

type MyComponentProps = {
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
};

function MyComponent({ style }: MyComponentProps) {
hayata-suenaga marked this conversation as resolved.
Show resolved Hide resolved
return <Animated.View style={style} />;
}

function App() {
const anim = useRef(new Animated.Value(0)).current;
return <MyComponent style={{opacity: anim.interpolate({...})}} />;
}
```

<a name="render-prop"></a><a name="1.5"></a>

- [1.5](#render-prop) **Render Prop**

```tsx
type ParentComponentProps = {
children: (label: string) => React.ReactNode;
};

function ParentComponent({ children }: ParentComponentProps) {
return children("String being injected");
}

function App() {
return (
<ParentComponent>
{(label) => (
<View>
<Text>{label}</Text>
</View>
)}
</ParentComponent>
);
}
```

<a name="type-narrowing"></a><a name="1.6"></a>

- [1.6](#type-narrowing) **Type Narrowing** Narrow types down using `typeof`, discriminated unions, or custom type guards. Refer [this guide](https://medium.com/@hayata.suenaga/discriminated-unions-custom-type-guards-182ebe1f92fb) for more information on when to use discriminated unions and custom type guards.
hayata-suenaga marked this conversation as resolved.
Show resolved Hide resolved

```ts
type Manager = {
role: "manager";
team: string;
};

type Engineer = {
role: "engineer";
language: "ts" | "js" | "php";
};

function introduce(employee: Manager | Engineer) {
console.log(employee.team); // TypeScript errors: Property 'team' does not exist on type 'Manager | Engineer'.

if (employee.role === "manager") {
console.log(`I manage ${employee.team}`); // employee: Manager
} else {
console.log(`I write ${employee.language}`); // employee: Engineer
}
}
```

In the above code, type narrowing is used to determine whether an employee object is a Manager or an Engineer based on the role property, allowing safe access to the `team` property for managers and the `language` property for engineers.

We can also create a custom type guard function.

```ts
function isManager(employee: Manager | Engineer): employee is Manager {
return employee.role === "manager";
}

function introduce(employee: Manager | Engineer) {
if (isManager(employee)) {
console.log(`I manage ${employee.team}`); // employee: Manager
}
}
```

In the above code, `employee is Manager` is a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates). It means that the return type of `isManager` is a `boolean` that indicates whether a value passed to the function is of a certain type (e.g. `Manager`).

<a name="try-catch-clauses"></a><a name="1.7"></a>

- [1.7](#try-catch-clauses) **Error in Try-Catch Clauses**

Errors in try/catch clauses are inferred as `unknown`. If the error data needs to be accessed, the type of the error needs to be checked and narrowed down.

```ts
try {
....
} catch (e) { // `e` is `unknown`.
if (e instanceof Error) {
// you can access properties on Error
console.error(e.message);
}
}
```

<a name="const-assertion"></a><a name="1.8"></a>

- [1.8](#const-assertion) **Use const assertions for rigorous typing**

Use `as const` when you want to ensure that the types and values are as exact as possible and prevent unwanted mutations.

```ts
const greeting1 = "hello"; // type: string
const greeting2 = "goodbye" as const; // type: "goodbye"

const person1 = { name: "Alice", age: 20 }; // type: { name: string, age: number }
const person2 = { name: "Bob", age: 30 } as const; // type: { readonly name: "Bob", readonly age: 30 }

const array1 = ["hello", 1]; // type: (string | number)[]
hayata-suenaga marked this conversation as resolved.
Show resolved Hide resolved
const array2 = ["goodbye", 2]; // type: readonly ["goodbye", 2]
```

<a name="higher-order-components"></a><a name="1.9"></a>

- [1.9](#higher-order-components) **Higher Order Components**

Typing HOCs is hard. Refer to [this article](https://medium.com/@hayata.suenaga/ts-higher-order-components-30c38dd19ae8) for detailed guideline on typing HOCs for different usages of HOCs.

<a name="function-overloading"></a><a name="1.10"></a>

- [1.10](#function-overloading) **Function Overloading**

Use [function overloads](https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads) to provide more type information for functions. For the following types of functions, function overloading can be beneficial.

- The return type depends on the input type
- When function accepts different number of parameters
- There are type dependencies between parameters

Refer to [this guide](https://medium.com/@hayata.suenaga/when-to-use-function-overloads-acc48f7e3142) to learn how to use functional overloads for each situation.
hayata-suenaga marked this conversation as resolved.
Show resolved Hide resolved
Loading