Skip to content

Latest commit

 

History

History
470 lines (343 loc) · 16.5 KB

TYPESCRIPT.md

File metadata and controls

470 lines (343 loc) · 16.5 KB

Tulsa Web Devs TypeScript Style Guide

Table of Contents

General Rules

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>;
};

Guidelines

  • 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 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.

      // 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 use d.ts file extension even when a file contains only type declarations. Only exceptions are src/types/global.d.ts and src/types/modules/*.d.ts files in which third party packages 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. Use type. eslint: @typescript-eslint/consistent-type-definitions

    Why? In addition to consistency, this artile explains other benefits of using type over interface.

    // 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 use any. Use unknown if type is not known beforehand. eslint: @typescript-eslint/no-explicit-any

    Why? any type bypasses type checking. unknown is type safe as unknown 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>: Use T[] or readonly T[] for simple types (i.e. types which are just primitive names or type references). Use Array<T> or ReadonlyArray<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 and defaultProps: 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 use object 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 for Record<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 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.16 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.17 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>;

Communication Items

Tag a maintainer on GitHub 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/ts-env.d.ts.

// ts-env.d.ts

declare module 'external-library-name' {
  interface LibraryComponentProps {
    // Add or modify typings
    additionalProp: string;
  }
}

If the error cannot be fixed via module augmentation, add //@ts-expect-error only once, at the source of the error (not every usage) and create a separate GH issue. Prefix the issue title with [TS ERROR #<issue-number-of-original-PR>]. Cross link the original PR or issue 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-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

Footnotes

  1. This is because skipLibCheck TypeScript configuration is set to true in this project.