SSW Foursquare

Rules to Better TypeScript - 6 Rules

  1. Do you avoid using “any”?

    TypeScript’s any keyword is a blessing and a curse. It is a type that can be anything, where every possible property and method exists and also returns any. It can be casted to and from anything and is how you tell the compiler to get out of your way.

    However, it’s easy to use it as a crutch, and as a result, miss out on handy intellisense, refactoring support and compile-time safety – the main benefits of TypeScript!

    If you're trying to write more type-safe code, it's generally recommended to use "unknown" instead of "any" wherever possible, as it forces you to perform type checks and can help catch errors earlier in the development process.

    any bad
    Figure: Bad example – I can pass anything into this method, so I get bad output at run time (“undefined undefined”)

    any good
    Figure: Good example – using types means I get errors and intellisense support

    If you have ESLint enabled in your project, you can enable the no-explicit-any rule to provide useful lint warnings or errors to ensure the any type is not used in the project.

  2. Do you describe types sparsely?

    This comes down to personal preference, but there are only a few times when you must define a type in TypeScript, for example:

    1. When initializing a variable with an ambiguous value (eg. null)
    2. Function parameters

    Of course, there are also times when you may want to be more explicit – you may want to have an interface as a function return value instead of the class, for example.

    The rest of the time, rely on TypeScript to infer the type for you.

    describe
    Figure: Except for the input parameter, TypeScript can infer all the types for this function

  3. Do you know why to use const assertions instead of TypeScript enums?

    It's super important to ensure that magic strings are not used in your codebase. Typically, we would use constant values or enums to solve this problem, but this may not be applicable when using TypeScript. You might expect TypeScript enums to function like strongly typed languages like C# but often this is not the case.

    Video: Enums considered harmful (9 min)

    While TypeScript enums provide a lot of useful type safety at runtime, it's very important to consider that there may be cleaner options.

    Numerical Enums

    When you define an enum like this:

    enum Fruits {
        Apple,
        Banana,
        Cherry
    }

    When compiled to JavaScript, it looks like:

    var Fruits;
    (function (Fruits) {
        Fruits[Fruits["Apple"] = 0] = "Apple";
        Fruits[Fruits["Banana"] = 1] = "Banana";
        Fruits[Fruits["Cherry"] = 2] = "Cherry";
    })(Fruits || (Fruits = {}));

    However, this makes it hard to loop over the keys of the enum, as when you run Object.keys(Fruits) you would get the following array returned:

    ["0", "1", "2", "Apple", "Banana", "Cherry"] 

    Bad Example - an irritating DX, instead of returning just the values of the enum

    Instead, a much cleaner option is by using const assertions. With const assertions we can be sure the code is using the string values we want:

    const fruits = ["Apple", "Banana", "Cherry"] as const;

    Now, if we look into the content of the shapes array using:

    type Fruit = typeof fruits[number];

    We can construct this type from the above array, which is equivalent to:

    type Fruit = "Apple" | "Banana" | "Cherry";

    Good Example - a much cleaner DX

    This makes it super easy to loop over keys within a union type. This also allows us to be able to pass "Apple" into a function that takes Fruit as an argument. We get super useful feedback from our code editor - the same as a typical TypeScript union type from VSCode from the Fruit union type:

    vscode intellisense array2
    Figure: Working VSCode Intellisense that works with all const assertions

    String Enums

    enum Icon {
      sun = "sun",
      moon = "moon"
    }
    
    const icons: Record<Icon, string> = {
      sun: "sun_12345.jpg",
      moon: "moon_543212.jpg"
    };

    Bad Example - duplication of key values where it is not needed

    This is problematic, as it provides us no useful type hints for object values, as object values are typed as string, and there is an unecessary duplication of object keys. For cases like this with a single source of truth (i.e. the icons object), we can use const assertions, similiar to above with objects:

    const icons = {
      sun: "sun_12345.jpg",
      moon: "moon_543212.jpg",
    } as const;
    
    type IconKey = keyof typeof icons; // "sun" | "moon" union type
    
    type Icon = (typeof icons)[IconKey]; // "sun_12345.jpg" | "moon_543212.jpg" union type

    Good Example - a much cleaner DX with a single source of truth in the as const object

    Similar to the array const assertion above, these also provide useful type hints in your code editor:

    icon vscode sense
    Figure: Using the Icon type from above

    iconkey vscode sense
    Figure: The IconKey type from above

    Remember, it's important to assess on a case-by-case basis when you are writing code to determine whether a const assertion can be used instead of an enum. For example, it's important when dealing with object values you don't want to overlap (i.e. Icon1, Icon2 both with a moon key but with different moon images) to opt for enums. However, using const assertions will likely lead to better DX (Developer eXperience) in most cases.

  4. Do you follow good Object-Oriented design patterns?

    TypeScript gives us a reasonably full-featured object-oriented system, and we should use it as such. Following the SOLID and DRY principles are encouraged.

    Write code that you’d be proud to see in C#, because there are no longer any excuses.

  5. Do you have good TypeScript configuration?

    TypeScript is a powerful language that transpiles to JavaScript, and provides much desired type-safety and IDE refactoring support. But without good configuration, a lot of the benefits can be lost.

    Use tsconfig.json

    Putting a “tsconfig.json” file in your project tells the typescript compiler where the root of your project is, and provides a centralized place to configure the compiler. This config is read by IDEs and the compiler and can be utilised by the build scripts to ensure configuration is consistent.

    goodtypescriptconfig1
    Figure: A tsconfig.json file with great configuration

    Disable implicit “any”

    The primary benefit of TypeScript is type-safety, and attempting to escape from the type-safety should be a conscientious decision by the developer. So ensure that noImplicitAny is true, and keep your code type-aware and able to be refactored.

    Exclude external files

    By default, the compiler will compile everything ending in .ts. This means things inside node_modules and even typings will be parsed and included. Ensure you exclude these files to reduce your compile time and, more importantly, reduce your reported errors.

    Don’t rely on TypeScript for bundling

    TypeScript should compile in-place, and a single file input should produce a single file output. This reduces compile time, and puts bundling in the hands of a system that knows more about the modules – the module loader.

    Hide generated files from your IDE

    Files generated from typescript get in the way – you don’t want to scroll through .d.ts, .js and .js.map files all the time. So hide them in the IDE.In VSCode this can be done via the “files.exclude” key in the settings.json file. For a shared experience across the team, check this file into source control.

    goodtypescriptconfig2
    Figure: VSCode settings.json file that hides generated files

  6. Do you only export what is necessary?

    Each file in TypeScript is a module, and each module can export whatever members it wants.  However, if you export everything, you run the risk of having to increment major versions (when using semantic versioning), or having your module used in unintended ways.

    Only export the types necessary to reduce your API surface.  Often, this means exporting interfaces over implementations.

We open source. Powered by GitHub