- Other Expensify Resources on TypeScript
- General Rules
- Guidelines
- 1.1 Naming Conventions
- 1.2
d.ts
Extension - 1.3 Type Alias vs. Interface
- 1.4 Enum vs. Union Type
- 1.5
unknown
vs.any
- 1.6
T[]
vs.Array<T>
- 1.7 @ts-ignore
- 1.8 Optional chaining and nullish coalescing
- 1.9 Type Inference
- 1.10 JSDoc
- 1.11
propTypes
anddefaultProps
- 1.12 Utility Types
- 1.13
object
Type - 1.14 Export Prop Types
- 1.15 File Organization
- 1.16 Reusable Types
- 1.17
.tsx
- 1.18 No inline prop types
- 1.19 Satisfies operator
- 1.20 Hooks instead of HOCs
- 1.21
compose
usage - 1.22 Type imports
- Exception to Rules
- Communication Items
- Migration Guidelines
- Learning Resources
Strive to type as strictly as possible.
type Foo = {
fetchingStatus: "loading" | "success" | "error"; // vs. fetchingStatus: string;
person: { name: string; age: number }; // vs. person: Record<string, unknown>;
};
-
1.1 Naming Conventions: Follow naming conventions specified below
-
Use PascalCase for type names. eslint:
@typescript-eslint/naming-convention
// BAD type foo = ...; type BAR = ...; // GOOD type Foo = ...; type Bar = ...;
-
Do not postfix type aliases with
Type
.// BAD type PersonType = ...; // GOOD type Person = ...;
-
Use singular name for union types.
// BAD type Colors = "red" | "blue" | "green"; // GOOD type Color = "red" | "blue" | "green";
-
Use
{ComponentName}Props
pattern for prop types.// BAD type Props = { // component's props }; function MyComponent({}: Props) { // component's code } // GOOD type MyComponentProps = { // component's props }; function MyComponent({}: MyComponentProps) { // component's code }
-
For generic type parameters, use
T
if you have only one type parameter. Don't use theT
,U
,V
... sequence. Make type parameter names descriptive, each prefixed withT
.Prefix each type parameter name to distinguish them from other types.
// BAD type KeyValuePair<T, U> = { key: K; value: U }; type Keys<Key> = Array<Key>; // GOOD type KeyValuePair<TKey, TValue> = { key: TKey; value: TValue }; type Keys<T> = Array<T>; type Keys<TKey> = Array<TKey>;
-
-
1.2
d.ts
Extension: Do not used.ts
file extension even when a file contains only type declarations. Only exceptions aresrc/types/global.d.ts
andsrc/types/modules/*.d.ts
files in which third party packages and JavaScript's built-in modules (e.g.window
object) can be modified using module augmentation. Refer to the Communication Items section to learn more about module augmentation.Why? Type errors in
d.ts
files are not checked by TypeScript 1.
-
1.3 Type Alias vs. Interface: Do not use
interface
. Usetype
. eslint:@typescript-eslint/consistent-type-definitions
Why? In TypeScript,
type
andinterface
can be used interchangeably to declare types. Usetype
for consistency.// BAD interface Person { name: string; } // GOOD type Person = { name: string; };
-
1.4 Enum vs. Union Type: Do not use
enum
. Use union types. eslint:no-restricted-syntax
Why? Enums come with several pitfalls. Most enum use cases can be replaced with union types.
// 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<typeof COLORS>; // 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<typeof COLORS>; // type: 'red' | 'green' | 'blue' printColor(COLORS.Red);
-
1.5
unknown
vs.any
: Don't useany
. Useunknown
if type is not known beforehand. eslint:@typescript-eslint/no-explicit-any
Why?
any
type bypasses type checking.unknown
is type safe asunknown
type needs to be type narrowed before being used.const value: unknown = JSON.parse(someJson); if (typeof value === 'string') {...} else if (isPerson(value)) {...} ...
-
1.6
T[]
vs.Array<T>
: UseT[]
orreadonly T[]
for simple types (i.e. types which are just primitive names or type references). UseArray<T>
orReadonlyArray<T>
for all other types (union types, intersection types, object types, function types, etc). eslint:@typescript-eslint/array-type
// Array<T> const a: Array<string | number> = ["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: 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 for specific instructions on how to deal with type errors during the migration. eslint:@typescript-eslint/ban-ts-comment
-
1.8 Optional chaining and nullish coalescing: Use optional chaining and nullish coalescing instead of the
get
lodash function. eslint:no-restricted-imports
// BAD import lodashGet from "lodash/get"; const name = lodashGet(user, "name", "default name"); // GOOD const name = user?.name ?? "default name";
-
1.9 Type Inference: When possible, allow the compiler to infer type of variables.
// BAD const foo: string = "foo"; const [counter, setCounter] = useState<number>(0); // GOOD const foo = "foo"; const [counter, setCounter] = useState(0); const [username, setUsername] = useState<string | undefined>(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.
function simpleFunction(name: string) { return `hello, ${name}`; } function complicatedFunction(name: string): boolean { // ... some complex logic here ... return foo; }
-
1.10 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
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.
// 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
anddefaultProps
: Do not use them. Use object destructing to assign default values if necessary.Refer to the propTypes Migration Table on how to type props based on existing
propTypes
.Assign a default value to each optional prop unless the default values is
undefined
.type MyComponentProps = { requiredProp: string; optionalPropWithDefaultValue?: number; optionalProp?: boolean; }; function MyComponent({ requiredProp, optionalPropWithDefaultValue = 42, optionalProp, }: MyComponentProps) { // component's code }
-
1.12 Utility Types: Use types from TypeScript utility types and
type-fest
when possible.type Foo = { bar: string; }; // BAD type ReadOnlyFoo = { readonly [Property in keyof Foo]: Foo[Property]; }; // GOOD type ReadOnlyFoo = Readonly<Foo>;
-
1.13
object
: Don't useobject
type. eslint:@typescript-eslint/ban-types
Why?
object
refers to "any non-primitive type," not "any object". Typing "any non-primitive value" is not commonly needed.// 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<string, unknown>
.Even though
string
is specified as a key,Record<string, unknown>
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 forRecord<string, unknown>
.function logObject(object: Record<string, unknown>) { for (const [key, value] of Object.entries(object)) { console.log(`${key}: ${value}`); } }
-
1.14 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.
// MyComponent.tsx export type MyComponentProps = { foo: string; }; export default function MyComponent({ foo }: MyComponentProps) { return <Text>{foo}</Text>; } // BAD import { ComponentProps } from "React"; import MyComponent from "./MyComponent"; type MyComponentProps = ComponentProps<typeof MyComponent>; // GOOD import MyComponent, { MyComponentProps } from "./MyComponent";
-
1.15 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. Do not usesatisfies
operator for platform-specific implementations, always define shared types that complies with all variants.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 for further information.Utility module example
// 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
// 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 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.// src/types/Report.ts type Report = {...}; export default Report;
-
1.17 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: Do not define prop types inline for components that are exported.
Why? Prop types might need to be exported from component files. If the component is only used inside a file or module and not exported, then inline prop types can be used.
// 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 }
-
1.19 Satisfies Operator: Use the
satisfies
operator when you need to validate that the structure of an expression matches a specific type, without affecting the resulting type of the expression.Why? TypeScript developers often want to ensure that an expression aligns with a certain type, but they also want to retain the most specific type possible for inference purposes. The
satisfies
operator assists in doing both.// BAD const sizingStyles = { w50: { width: '50%', }, mw100: { maxWidth: '100%', }, } as const; // GOOD const sizingStyles = { w50: { width: '50%', }, mw100: { maxWidth: '100%', }, } satisfies Record<string, ViewStyle>;
-
1.20 Hooks instead of HOCs: Replace HOCs usage with Hooks whenever possible.
Why? Hooks are easier to use (can be used inside the function component), and don't need nesting or
compose
when exporting the component. It also allows us to removecompose
completely in some components since it has been bringing up some issues with TypeScript. Read thecompose
usage section for further information about the TypeScript issues withcompose
.Note: Because Onyx doesn't provide a hook yet, in a component that accesses Onyx data with
withOnyx
HOC, please make sure that you don't use other HOCs (if applicable) to avoid HOC nesting.// BAD type ComponentOnyxProps = { session: OnyxEntry<Session>; }; type ComponentProps = WindowDimensionsProps & WithLocalizeProps & ComponentOnyxProps & { someProp: string; }; function Component({windowWidth, windowHeight, translate, session, someProp}: ComponentProps) { // component's code } export default compose( withWindowDimensions, withLocalize, withOnyx<ComponentProps, ComponentOnyxProps>({ session: { key: ONYXKEYS.SESSION, }, }), )(Component); // GOOD type ComponentOnyxProps = { session: OnyxEntry<Session>; }; type ComponentProps = ComponentOnyxProps & { someProp: string; }; function Component({session, someProp}: ComponentProps) { const {windowWidth, windowHeight} = useWindowDimensions(); const {translate} = useLocalize(); // component's code } // There is no hook alternative for withOnyx yet. export default withOnyx<ComponentProps, ComponentOnyxProps>({ session: { key: ONYXKEYS.SESSION, }, })(Component);
-
1.21
compose
usage: Avoid the usage ofcompose
function to compose HOCs in TypeScript files. Use nesting instead.Why?
compose
function doesn't work well with TypeScript when dealing with several HOCs being used in a component, many times resulting in wrong types and errors. Instead, nesting can be used to allow a seamless use of multiple HOCs and result in a correct return type of the compoment. Also, you can use hooks instead of HOCs whenever possible to minimize or even remove the need of HOCs in the component.// BAD export default compose( withCurrentUserPersonalDetails, withReportOrNotFound(), withOnyx<ComponentProps, ComponentOnyxProps>({ session: { key: ONYXKEYS.SESSION, }, }), )(Component); // GOOD export default withCurrentUserPersonalDetails( withReportOrNotFound()( withOnyx<ComponentProps, ComponentOnyxProps>({ session: { key: ONYXKEYS.SESSION, }, })(Component), ), ); // GOOD - alternative to HOC nesting const ComponentWithOnyx = withOnyx<ComponentProps, ComponentOnyxProps>({ session: { key: ONYXKEYS.SESSION, }, })(Component); const ComponentWithReportOrNotFound = withReportOrNotFound()(ComponentWithOnyx); export default withCurrentUserPersonalDetails(ComponentWithReportOrNotFound);
-
1.22 Type imports/exports: Always use the
type
keyword when importing/exporting typesWhy? In order to improve code clarity and consistency and reduce bundle size after typescript transpilation, we enforce the all type imports/exports to contain the
type
keyword. This way, TypeScript can automatically remove those imports from the transpiled JavaScript bundleImports:
// BAD import {SomeType} from './a' import someVariable from './a' import {someVariable, SomeOtherType} from './b' // GOOD import type {SomeType} from './a' import someVariable from './a'
Exports:
// BAD export {SomeType} export someVariable // or export {someVariable, SomeOtherType} // GOOD export type {SomeType} export someVariable
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.
Comment in the
#expensify-open-source
Slack channel if any of the following situations are encountered. Each comment should be prefixed withTS 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 or JavaScript's built-in module are 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/types/modules/*.d.ts
, each library as a separate file.
// external-library-name.d.ts
declare module "external-library-name" {
interface LibraryComponentProps {
// Add or modify typings
additionalProp: string;
}
}
This section contains instructions that are applicable during the migration.
-
🚨 DO NOT write new code in TypeScript yet. The only time you write TypeScript code is when the file you're editing has already been migrated to TypeScript by the migration team, or when you need to add new files under
src/libs
,src/hooks
,src/styles
, andsrc/languages
directories. This guideline will be updated once it's time for new code to be written in TypeScript. If you're doing a major overhaul or refactoring of particular features or utilities of App and you believe it might be beneficial to migrate relevant code to TypeScript as part of the refactoring, please ask in the #expensify-open-source channel about it (and prefix your message withTS ATTENTION:
). -
If you're migrating a module that doesn't have a default implementation (i.e.
index.ts
, e.g.getPlatform
), convertindex.website.js
toindex.ts
. Withoutindex.ts
, TypeScript cannot get type information where the module is imported. -
Deprecate the usage of
underscore
. Use vanilla methods from JS instead. Only uselodash
when there is no easy vanilla alternative (eg.lodashMerge
). eslint:no-restricted-imports
// BAD
var arr = [];
_.each(arr, () => {});
// GOOD
var arr = [];
arr.forEach(() => {});
// BAD
lodashGet(object, ['foo'], 'bar');
// GOOD
object?.foo ?? 'bar';
-
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 #<issue-number-of-migration-PR>]
. 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 withTODO:
.
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-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.
-
The TS issue I'm working on is blocked by another TS issue because of type errors. What should I do?
In order to proceed with the migration faster, we are now allowing the use of
@ts-expect-error
annotation to temporally suppress those errors and help you unblock your issues. The only requirements is that you MUST add the annotation with a comment explaining that it must be removed when the blocking issue is migrated, e.g.:return ( <MenuItem // @ts-expect-error TODO: Remove this once MenuItem (https://github.com/Expensify/App/issues/25144) is migrated to TypeScript. wrapperStyle={styles.mr3} key={text} icon={icon} title={text} onPress={onPress} /> );
You will also need to reference the blocking issue in your PR. You can find all the TS issues here.
- Get up to speed quickly
- TypeScript playground
- Go though all examples on the playground. Click on "Example" tab on the top
- TypeScript playground
- Handy Reference
- TypeScript with React
Footnotes
-
This is because
skipLibCheck
TypeScript configuration is set totrue
in this project. ↩