-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[No QA] Add TypeScript Guideline #21549
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
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
37dcae5
add ts guidelines
c218adc
fix issues
b40a302
add section on file organization
842bc5f
address PR reviews
d89fc4c
add conversion example code
42ef632
add more sections
de56cb2
address PR reviews
0a0bf3e
specific path
bf2005f
add tsx section
6d9bf6d
add more guidelines on naming convention and prop types
0dd70cd
fix typos
593ba9e
fix typos & improve clarity
18b1522
add sections & change order of sections
01d3234
fix typos
e0826fd
change section order and improve code examples
a1021d2
edit guideline on rule exceptions & fix typos
904c6d6
address TS reviews
2fd543a
fix typos
6450b65
remove usage of any & fix typo
8b3bad5
address PR reviews
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 it isn't possible to determine the internal structure, use `Record<string, unknown>`. | | ||
| | `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; | ||
| }; | ||
|
|
||
| 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; | ||
| }; | ||
| ``` | ||
|
hayata-suenaga marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.
|
||
|
|
||
| <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.
|
||
| 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 to [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. | ||
|
|
||
| ```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.
|
||
| const array2 = ["goodbye", 2] as const; // 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.
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.