diff --git a/contributingGuides/PROPTYPES_CONVERSION_TABLE.md b/contributingGuides/PROPTYPES_CONVERSION_TABLE.md new file mode 100644 index 000000000000..4ef1cd5ca655 --- /dev/null +++ b/contributingGuides/PROPTYPES_CONVERSION_TABLE.md @@ -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` or `unknown` | Figure out what would be the correct data type and use it.

If you know that it's a object but isn't possible to determine the internal structure, use `Record`. | +| `PropTypes.array` or `PropTypes.arrayOf(T)` | `T[]` or `Array` | Convert to `T[]`, where `T` is the data type of the array.

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.

If you want an object but it isn't possible to determine the internal structure, use `Record`. | +| `PropTypes.objectOf(T)` | `Record` | Convert to a `Record` where `T` is the data type of values stored in the object.

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; + inputs: Record; + label: string; + anchor: React.ReactNode; + footer: React.ReactElement; + uniqSymbol: symbol; + icon: React.ElementType; + date: Date; + size: Size; + optionalString?: string; +}; +``` diff --git a/contributingGuides/TS_CHEATSHEET.md b/contributingGuides/TS_CHEATSHEET.md new file mode 100644 index 000000000000..df6d70b5ae90 --- /dev/null +++ b/contributingGuides/TS_CHEATSHEET.md @@ -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 + + + +- [1.1](#children-prop) **`props.children`** + + ```tsx + type WrapperComponentProps = { + children?: React.ReactNode; + }; + + function WrapperComponent({ children }: WrapperComponentProps) { + return {children}; + } + + function App() { + return ( + + + + ); + } + ``` + + + +- [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( + (props, ref) => { + return ( + + + {props.children} + + ); + } + ); + + function ParentComponent() { + const ref = useRef(); + return ; + } + ``` + + + +- [1.3](#style-props) **Style Props** + + Use `StyleProp` 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; + textStyle?: StyleProp; + imageStyle?: StyleProp; + }; + + function MyComponent({ containerStyle, textStyle, imageStyle }: MyComponentProps) = { + + Sample Image + + + } + ``` + + + +- [1.4](#animated-style) **Animated styles** + + ```ts + import {useRef} from 'react'; + import {Animated, StyleProp, ViewStyle} from 'react-native'; + + type MyComponentProps = { + style?: Animated.WithAnimatedValue>; + }; + + function MyComponent({ style }: MyComponentProps) { + return ; + } + + function App() { + const anim = useRef(new Animated.Value(0)).current; + return ; + } + ``` + + + +- [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 ( + + {(label) => ( + + {label} + + )} + + ); + } + ``` + + + +- [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`). + + + +- [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); + } + } + ``` + + + +- [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)[] + const array2 = ["goodbye", 2] as const; // type: readonly ["goodbye", 2] + ``` + + + +- [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. + + + +- [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. diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md new file mode 100644 index 000000000000..965ce24a5357 --- /dev/null +++ b/contributingGuides/TS_STYLE.md @@ -0,0 +1,525 @@ +# Expensify TypeScript Style Guide + +## Table of Contents + +- [Other Expensify Resources on TypeScript](#other-expensify-resources-on-typescript) +- [General Rules](#general-rules) +- [Guidelines](#guidelines) + - [1.1 Naming Conventions](#naming-conventions) + - [1.2 `d.ts` Extension](#d-ts-extension) + - [1.3 Type Alias vs. Interface](#type-alias-vs-interface) + - [1.4 Enum vs. Union Type](#enum-vs-union-type) + - [1.5 `unknown` vs. `any`](#unknown-vs-any) + - [1.6 `T[]` vs. `Array`](#array) + - [1.7 @ts-ignore](#ts-ignore) + - [1.8 Optional chaining and nullish coalescing](#ts-nullish-coalescing) + - [1.9 Type Inference](#type-inference) + - [1.10 JSDoc](#jsdoc) + - [1.11 `propTypes` and `defaultProps`](#proptypes-and-defaultprops) + - [1.12 Utility Types](#utility-types) + - [1.13 `object` Type](#object-type) + - [1.14 Export Prop Types](#export-prop-types) + - [1.15 File Organization](#file-organization) + - [1.16 Reusable Types](#reusable-types) + - [1.17 `.tsx`](#tsx) + - [1.18 No inline prop types](#no-inline-prop-types) +- [Exception to Rules](#exception-to-rules) +- [Communication Items](#communication-items) +- [Migration Guidelines](#migration-guidelines) +- [Learning Resources](#learning-resources) + +## Other Expensify Resources on TypeScript + +- [Expensify TypeScript React Native CheatSheet](./TS_CHEATSHEET.md) +- [Expensify TypeScript PropTypes Conversion Table](./PROPTYPES_CONVERSION_TABLE.md) + +## General Rules + +Strive to type as strictly as possible. + +```ts +type Foo = { + fetchingStatus: "loading" | "success" | "error"; // vs. fetchingStatus: string; + person: { name: string; age: number }; // vs. person: Record; +}; +``` + +## Guidelines + + + +- [1.1](#naming-conventions) **Naming Conventions**: Follow naming conventions specified below + + - Use PascalCase for type names. eslint: [`@typescript-eslint/naming-convention`](https://typescript-eslint.io/rules/naming-convention/) + + ```ts + // BAD + type foo = ...; + type BAR = ...; + + // GOOD + type Foo = ...; + type Bar = ...; + ``` + + - Do not postfix type aliases with `Type`. + + ```ts + // BAD + type PersonType = ...; + + // GOOD + type Person = ...; + ``` + + - Use singular name for union types. + + ```ts + // BAD + type Colors = "red" | "blue" | "green"; + + // GOOD + type Color = "red" | "blue" | "green"; + ``` + + - For generic type parameters, use `T` if you have only one type parameter. Don't use the `T`, `U`, `V`... sequence. Make type parameter names descriptive, each prefixed with `T`. + + > Prefix each type parameter name to distinguish them from other types. + + ```ts + // BAD + type KeyValuePair = { key: K; value: U }; + + type Keys = Array; + + // GOOD + type KeyValuePair = { key: TKey; value: TValue }; + + type Keys = Array; + type Keys = Array; + ``` + + + +- [1.2](#d-ts-extension) **`d.ts` Extension**: Do not use `d.ts` file extension even when a file contains only type declarations. Only exception is the `global.d.ts` file in which third party packages can be modified using module augmentation. Refer to the [Communication Items](#communication-items) section to learn more about module augmentation. + + > Why? Type errors in `d.ts` files are not checked by TypeScript [^1]. + +[^1]: This is because `skipLibCheck` TypeScript configuration is set to `true` in this project. + + + +- [1.3](#type-alias-vs-interface) **Type Alias vs. Interface**: Do not use `interface`. Use `type`. eslint: [`@typescript-eslint/consistent-type-definitions`](https://typescript-eslint.io/rules/consistent-type-definitions/) + + > Why? In TypeScript, `type` and `interface` can be used interchangeably to declare types. Use `type` for consistency. + + ```ts + // BAD + interface Person { + name: string; + } + + // GOOD + type Person = { + name: string; + }; + ``` + + + +- [1.4](#enum-vs-union-type) **Enum vs. Union Type**: Do not use `enum`. Use union types. eslint: [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax) + + > Why? Enums come with several [pitfalls](https://blog.logrocket.com/why-typescript-enums-suck/). Most enum use cases can be replaced with union types. + + ```ts + // Most simple form of union type. + type Color = "red" | "green" | "blue"; + function printColors(color: Color) { + console.log(color); + } + + // When the values need to be iterated upon. + import { TupleToUnion } from "type-fest"; + + const COLORS = ["red", "green", "blue"] as const; + type Color = TupleToUnion; // type: 'red' | 'green' | 'blue' + + for (const color of COLORS) { + printColor(color); + } + + // When the values should be accessed through object keys. (i.e. `COLORS.Red` vs. `"red"`) + import { ValueOf } from "type-fest"; + + const COLORS = { + Red: "red", + Green: "green", + Blue: "blue", + } as const; + type Color = ValueOf; // type: 'red' | 'green' | 'blue' + + printColor(COLORS.Red); + ``` + + + +- [1.5](#unknown-vs-any) **`unknown` vs. `any`**: Don't use `any`. Use `unknown` if type is not known beforehand. eslint: [`@typescript-eslint/no-explicit-any`](https://typescript-eslint.io/rules/no-explicit-any/) + + > Why? `any` type bypasses type checking. `unknown` is type safe as `unknown` type needs to be type narrowed before being used. + + ```ts + const value: unknown = JSON.parse(someJson); + if (typeof value === 'string') {...} + else if (isPerson(value)) {...} + ... + ``` + + + +- [1.6](#array) **`T[]` vs. `Array`**: Use `T[]` or `readonly T[]` for simple types (i.e. types which are just primitive names or type references). Use `Array` or `ReadonlyArray` for all other types (union types, intersection types, object types, function types, etc). eslint: [`@typescript-eslint/array-type`](https://typescript-eslint.io/rules/array-type/) + + ```ts + // Array + const a: Array = ["a", "b"]; + const b: Array<{ prop: string }> = [{ prop: "a" }]; + const c: Array<() => void> = [() => {}]; + + // T[] + const d: MyType[] = ["a", "b"]; + const e: string[] = ["a", "b"]; + const f: readonly string[] = ["a", "b"]; + ``` + + + +- [1.7](#ts-ignore) **@ts-ignore**: Do not use `@ts-ignore` or its variant `@ts-nocheck` to suppress warnings and errors. + + > Use `@ts-expect-error` during the migration for type errors that should be handled later. Refer to the [Migration Guidelines](#migration-guidelines) for specific instructions on how to deal with type errors during the migration. eslint: [`@typescript-eslint/ban-ts-comment`](https://typescript-eslint.io/rules/ban-ts-comment/) + + + +- [1.8](#ts-nullish-coalescing) **Optional chaining and nullish coalescing**: Use optional chaining and nullish coalescing instead of the `get` lodash function. eslint: [`no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports) + + ```ts + // BAD + import lodashGet from "lodash/get"; + const name = lodashGet(user, "name", "default name"); + + // GOOD + const name = user?.name ?? "default name"; + ``` + + + +- [1.9](#type-inference) **Type Inference**: When possible, allow the compiler to infer type of variables. + + ```ts + // BAD + const foo: string = "foo"; + const [counter, setCounter] = useState(0); + + // GOOD + const foo = "foo"; + const [counter, setCounter] = useState(0); + const [username, setUsername] = useState(undefined); // Username is a union type of string and undefined, and its type cannot be inferred from the default value of undefined + ``` + + For function return types, default to always typing them unless a function is simple enough to reason about its return type. + + > Why? Explicit return type helps catch errors when implementation of the function changes. It also makes it easy to read code even when TypeScript intellisense is not provided. + + ```ts + function simpleFunction(name: string) { + return `hello, ${name}`; + } + + function complicatedFunction(name: string): boolean { + // ... some complex logic here ... + return foo; + } + ``` + + + +- [1.10](#jsdoc) **JSDoc**: Omit comments that are redundant with TypeScript. Do not declare types in `@param` or `@return` blocks. Do not write `@implements`, `@enum`, `@private`, `@override`. eslint: [`jsdoc/no-types`](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.README/rules/no-types.md) + + > Not all parameters or return values need to be listed in the JSDoc comment. If there is no comment accompanying the parameter or return value, omit it. + + ```ts + // BAD + /** + * @param {number} age + * @returns {boolean} Whether the person is a legal drinking age or nots + */ + function canDrink(age: number): boolean { + return age >= 21; + } + + // GOOD + /** + * @returns Whether the person is a legal drinking age or nots + */ + function canDrink(age: number): boolean { + return age >= 21; + } + ``` + + In the above example, because the parameter `age` doesn't have any accompanying comment, it is completely omitted from the JSDoc. + + + +- [1.11](#proptypes-and-defaultprops) **`propTypes` and `defaultProps`**: Do not use them. Use object destructing to assign default values if necessary. + + > Refer to [the propTypes Migration Table](./PROPTYPES_CONVERSION_TABLE.md) on how to type props based on existing `propTypes`. + + > Assign a default value to each optional prop unless the default values is `undefined`. + + ```tsx + type MyComponentProps = { + requiredProp: string; + optionalPropWithDefaultValue?: number; + optionalProp?: boolean; + }; + + function MyComponent({ + requiredProp, + optionalPropWithDefaultValue = 42, + optionalProp, + }: MyComponentProps) { + // component's code + } + ``` + + + +- [1.12](#utility-types) **Utility Types**: Use types from [TypeScript utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) and [`type-fest`](https://github.com/sindresorhus/type-fest) when possible. + + ```ts + type Foo = { + bar: string; + }; + + // BAD + type ReadOnlyFoo = { + readonly [Property in keyof Foo]: Foo[Property]; + }; + + // GOOD + type ReadOnlyFoo = Readonly; + ``` + + + +- [1.13](#object-type) **`object`**: Don't use `object` type. eslint: [`@typescript-eslint/ban-types`](https://typescript-eslint.io/rules/ban-types/) + + > Why? `object` refers to "any non-primitive type," not "any object". Typing "any non-primitive value" is not commonly needed. + + ```ts + // BAD + const foo: object = [1, 2, 3]; // TypeScript does not error + ``` + + If you know that the type of data is an object but don't know what properties or values it has beforehand, use `Record`. + + > Even though `string` is specified as a key, `Record` type can still accepts objects whose keys are numbers. This is because numbers are converted to strings when used as an object index. Note that you cannot use [symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) for `Record`. + + ```ts + function logObject(object: Record) { + for (const [key, value] of Object.entries(object)) { + console.log(`${key}: ${value}`); + } + } + ``` + + + +- [1.14](#export-prop-types) **Prop Types**: Don't use `ComponentProps` to grab a component's prop types. Go to the source file for the component and export prop types from there. Import and use the exported prop types. + + > Don't export prop types from component files by default. Only export it when there is a code that needs to access the prop type directly. + + ```tsx + // MyComponent.tsx + export type MyComponentProps = { + foo: string; + }; + + export default function MyComponent({ foo }: MyComponentProps) { + return {foo}; + } + + // BAD + import { ComponentProps } from "React"; + import MyComponent from "./MyComponent"; + type MyComponentProps = ComponentProps; + + // GOOD + import MyComponent, { MyComponentProps } from "./MyComponent"; + ``` + + + +- [1.15](#file-organization) **File organization**: In modules with platform-specific implementations, create `types.ts` to define shared types. Import and use shared types in each platform specific files. + + > Why? To encourage consistent API across platform-specific implementations. If you're migrating module that doesn't have a default implement (i.e. `index.ts`, e.g. `getPlatform`), refer to [Migration Guidelines](#migration-guidelines) for further information. + + Utility module example + + ```ts + // types.ts + type GreetingModule = { + getHello: () => string; + getGoodbye: () => string; + }; + + // index.native.ts + import { GreetingModule } from "./types"; + function getHello() { + return "hello from mobile code"; + } + function getGoodbye() { + return "goodbye from mobile code"; + } + const Greeting: GreetingModule = { + getHello, + getGoodbye, + }; + export default Greeting; + + // index.ts + import { GreetingModule } from "./types"; + function getHello() { + return "hello from other platform code"; + } + function getGoodbye() { + return "goodbye from other platform code"; + } + const Greeting: GreetingModule = { + getHello, + getGoodbye, + }; + export default Greeting; + ``` + + Component module example + + ```ts + // types.ts + export type MyComponentProps = { + foo: string; + } + + // index.ios.ts + import { MyComponentProps } from "./types"; + + export MyComponentProps; + export default function MyComponent({ foo }: MyComponentProps) { /* ios specific implementation */ } + + // index.ts + import { MyComponentProps } from "./types"; + + export MyComponentProps; + export default function MyComponent({ foo }: MyComponentProps) { /* Default implementation */ } + ``` + + + +- [1.16](#reusable-types) **Reusable Types**: Reusable type definitions, such as models (e.g., Report), must have their own file and be placed under `src/types/`. The type should be exported as a default export. + + ```ts + // src/types/Report.ts + + type Report = {...}; + + export default Report; + ``` + + + +- [1.17](#tsx) **tsx**: Use `.tsx` extension for files that contain React syntax. + + > Why? It is a widely adopted convention to mark any files that contain React specific syntax with `.jsx` or `.tsx`. + + + +- [1.18](#no-inline-prop-types) **No inline prop types**: Do not define prop types inline for components that are exported. + + > Why? Prop types might [need to be exported from component files](#export-prop-types). If the component is only used inside a file or module and not exported, then inline prop types can be used. + + ```ts + // BAD + export default function MyComponent({ foo, bar }: { foo: string, bar: number }){ + // component implementation + }; + + // GOOD + type MyComponentProps = { foo: string, bar: number }; + export default MyComponent({ foo, bar }: MyComponentProps){ + // component implementation + } + ``` + +## Exception to Rules + +Most of the rules are enforced in ESLint or checked by TypeScript. If you think your particular situation warrants an exception, post the context in the `#expensify-open-source` Slack channel with your message prefixed with `TS EXCEPTION:`. The internal engineer assigned to the PR should be the one that approves each exception, however all discussion regarding granting exceptions should happen in the public channel instead of the GitHub PR page so that the TS migration team can access them easily. + +When an exception is granted, link the relevant Slack conversation in your PR. Suppress ESLint or TypeScript warnings/errors with comments if necessary. + +This rule will apply until the migration is done. After the migration, discussion on granting exception can happen inside the PR page and doesn't need take place in the Slack channel. + +## Communication Items + +> Comment in the `#expensify-open-source` Slack channel if any of the following situations are encountered. Each comment should be prefixed with `TS ATTENTION:`. Internal engineers will access each situation and prescribe solutions to each case. Internal engineers should refer to general solutions to each situation that follows each list item. + +- I think types definitions in a third party library is incomplete or incorrect + +When the library indeed contains incorrect or missing type definitions and it cannot be updated, use module augmentation to correct them. All module augmentation code should be contained in `/src/global.d.ts`. + +```ts +declare module "external-library-name" { + interface LibraryComponentProps { + // Add or modify typings + additionalProp: string; + } +} +``` + +## Migration Guidelines + +> This section contains instructions that are applicable during the migration. + +- If you're migrating a module that doesn't have a default implementation (i.e. `index.ts`, e.g. `getPlatform`), convert `index.website.js` to `index.ts`. Without `index.ts`, TypeScript cannot get type information where the module is imported. + +- Deprecate the usage of `underscore`. Use the corresponding methods from `lodash`. eslint: [`no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports) + +- Found type bugs. Now what? + + If TypeScript migration uncovers a bug that has been “invisible,” there are two options an author of a migration PR can take: + + - Fix issues if they are minor. Document each fix in the PR comment. + - Suppress a TypeScript error stemming from the bug with `@ts-expect-error`. Create a separate GH issue. Prefix the issue title with `[TS ERROR #]`. Cross-link the migration PR and the created GH issue. On the same line as `@ts-expect-error`, put down the GH issue number prefixed with `TODO:`. + + > The `@ts-expect-error` annotation tells the TS compiler to ignore any errors in the line that follows it. However, if there's no error in the line, TypeScript will also raise an error. + + ```ts + // @ts-expect-error TODO: #21647 + const x: number = "123"; // No TS error raised + + // @ts-expect-error + const y: number = 123; // TS error: Unused '@ts-expect-error' directive. + ``` + +## Learning Resources + +### Quickest way to learn TypeScript + +- Get up to speed quickly + - [TypeScript playground](https://www.typescriptlang.org/play?q=231#example) + - Go though all examples on the playground. Click on "Example" tab on the top +- Handy Reference + - [TypeScript CheatSheet](https://www.typescriptlang.org/cheatsheets) + - [Type](https://www.typescriptlang.org/static/TypeScript%20Types-ae199d69aeecf7d4a2704a528d0fd3f9.png) + - [Control Flow Analysis](https://www.typescriptlang.org/static/TypeScript%20Control%20Flow%20Analysis-8a549253ad8470850b77c4c5c351d457.png) +- TypeScript with React + - [React TypeScript CheatSheet](https://react-typescript-cheatsheet.netlify.app/) + - [List of built-in utility types](https://react-typescript-cheatsheet.netlify.app/docs/basic/troubleshooting/utilities) + - [HOC CheatSheet](https://react-typescript-cheatsheet.netlify.app/docs/hoc/)