React, Typescript, Tailwind CSS

TypeScript is JavaScript with added syntax for types.

TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale. TypeScript is designed for the development of large applications and transcompiles to JavaScript, ensuring that it can run in any environment where JavaScript is executed, including browsers, Node.js, and any ECMAScript 3 or later engines.


Tips and Tricks

Don't use Function type in TypeScript

  • You shouldn't use Function as a type. It represents any function.

  • Usually, you want to be more specific - like specifying the number of arguments, or what the function returns.

    eg: (a: string, b: number) => any

  • If you do want to represent a function that can take any number of arguments, and return any type, use (...args: any[]) => any.

Preferring Interfaces Over Intersections

interface Foo { prop: string }

type Bar = { prop: string };
  • Interfaces create a single flat object type that detects property conflicts, which are usually important to resolve! Intersections on the other hand just recursively merge properties, and in some cases produce never.

  • Interfaces also display consistently better, whereas type aliases to intersections can't be displayed in part of other intersections.

  • Type relationships between interfaces are cached, as opposed to intersection types as a whole.

  • A final noteworthy difference is that when checking against a target intersection type, every constituent is checked before checking against the "effective"/"flattened" type.

For this reason, extending types with interfaces/extends is suggested over creating intersection types.

- type Foo = Bar & Baz & {
-     someProp: string;
- }
+ interface Foo extends Bar, Baz {
+     someProp: string;
+ }

Using Type Annotations

  • Adding type annotations, especially return types, can save the compiler a lot of work. In part, this is because named types tend to be more compact than anonymous types (which the compiler might infer), which reduces the amount of time spent reading and writing declaration files (e.g. for incremental builds). Type inference is very convenient, so there's no need to do this universally - however, it can be a useful thing to try if you've identified a slow section of your code.
- import { otherFunc } from "other";
+ import { otherFunc, OtherType } from "other";

- export function func() {
+ export function func(): OtherType {
      return otherFunc();
  }

Strongly Type process.env

If you want to validate that all your environment variables are present at runtime, you can use a library like t3-env.

This leverages zod to validate your environment variables at runtime. Here's an example:

import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    OPEN_AI_API_KEY: z.string().min(1),
  },
  clientPrefix: "PUBLIC_",
  client: {
    PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
  },
  runtimeEnv: process.env,
});

Preferring Base Types Over Unions

interface WeekdaySchedule {
  day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday";
  wake: Time;
  startWork: Time;
  endWork: Time;
  sleep: Time;
}

interface WeekendSchedule {
  day: "Saturday" | "Sunday";
  wake: Time;
  familyMeal: Time;
  sleep: Time;
}

declare function printSchedule(schedule: WeekdaySchedule | WeekendSchedule);

Every time an argument is passed to printSchedule, it has to be compared to each element of the union. For a two-element union, this is trivial and inexpensive. However, if your union has more than a dozen elements, it can cause real problems in compilation speed. For instance, to eliminate redundant members from a union, the elements have to be compared pairwise, which is quadratic. This sort of check might occur when intersecting large unions, where intersecting over each union member can result in enormous types that then need to be reduced. One way to avoid this is to use subtypes, rather than unions.

interface Schedule {
  day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday";
  wake: Time;
  sleep: Time;
}

interface WeekdaySchedule extends Schedule {
  day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday";
  startWork: Time;
  endWork: Time;
}

interface WeekendSchedule extends Schedule {
  day: "Saturday" | "Sunday";
  familyMeal: Time;
}

declare function printSchedule(schedule: Schedule);

NoInfer: TypeScript 5.4's New Utility Type

  • NoInfer is a utility type that can be used to prevent TypeScript from inferring a type from inside a generic function.

  • This can be useful when you have multiple runtime parameters each referencing the same type parameter. NoInfer allows you to control which parameter TypeScript should infer the type from.

const returnWhatIPassedIn = <T>(value: T) => value;

const result = returnWhatIPassedIn("hello");
// ^ const result: "hello"
const returnWhatIPassedIn = <T>(value: NoInfer<T>) => value;

const result = returnWhatIPassedIn("hello");
// ^ const result: unknown

React.ReactNode vs JSX.Element vs React.ReactElement

  • JSX.Element and React.ReactElement are functionally the same type. They can be used interchangeably. They represent the thing that a JSX expression creates.
const node: JSX.Element = <div />;

const node2: React.ReactElement = <div />;
  • They can't be used to represent all the things that React can render, like strings and numbers. For that, use React.ReactNode.
const node: React.ReactNode = <div />;
const node2: React.ReactNode = "hello world";
const node3: React.ReactNode = 123;
const node4: React.ReactNode = undefined;
const node5: React.ReactNode = null;
  • In everyday use, you should use React.ReactNode. You rarely need to use the more specific type of JSX.Element.
Previous
Dynamic trio - React, Tailwind CSS and Typescript