Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript #25

Open
4 tasks
coryhouse opened this issue Sep 4, 2019 · 0 comments
Open
4 tasks

TypeScript #25

coryhouse opened this issue Sep 4, 2019 · 0 comments

Comments

@coryhouse
Copy link
Owner

coryhouse commented Sep 4, 2019

10 JS/TS features I avoid

Tips

1. Think in sets.

  • Every type is a Set of values.
  • Some Sets are infinite: string, object; some finite: boolean, undefined.
  • unknown is Universal Set (including all values), while never is Empty Set (including no value).
  • The & operator creates an Intersection. It creates a smaller set. It must have both.
  • The | operator creates a Union: a larger Set but potentially with fewer commonly available fields (if two object types are composed).

2. Understand declared type and narrowed type

  • A variable has two types associated with it at any specific point of code location: a declaration type and a narrowed type. You can narrow a type by checking values.

3. Use a discriminated union instead of optional fields

type Circle = { kind: 'circle'; radius: number };
type Rect = { kind: 'rect'; width: number; height: number };
type Shape = Circle | Rect;

Above, the kind property lets us discriminate. This gives us more type safety than merely using optional fields.

4. Use a type predicate to avoid type assertion

function isCircle(shape: Shape): shape is Circle {
    return shape.kind === 'circle';
}

tsconfig settings checklist for ideal safety

  • Enable strict mode
  • Enable noFallthroughCasesInSwitch
  • Enable noUncheckedIndexedAccess some argue it should be enabled by default
  • Set allowJs false
  • Enable forceConsistentCasingInFileNames

Key Blog posts, tweets, sites

Avoid Enums

Prefer string literal types or const objects over enums. String literal types video

  • Enums bloat the bundle because they generate a reverse mapping from the enum values to the enum names.
  • Enums cause confusion because they're the one TS feature that manifests at runtime and uses nominal typing instead of structural typing.

d.ts files

  • Only use d.TS files for js files. Avoid otherwise, since doing so creates a global type.
  • Set skipLibCheck true to improve perf. Shouldn’t need anyway. 

Handling Boundaries

Boundaries:
-Local storage
-User input
-Network
-Config-based or Conventions
-File system
-Database requests

Ways to handle boundaries

  1. Write type guards/type assertion functions. To avoid having to write type guards by hand, Zod, Valibot, tiny-invariant, typia, and many others.
  2. Use tools that generate types. GraphQL CodeGen, Prisma, Open API, tRPC
  3. Inform TS of your convention/configuration - Tanstack Router

Convert a large existing project from JS to TS via a script:

https://github.com/airbnb/ts-migrate

Cheat sheets

Utility libraries

Pattern matching via ts-pattern
type-fest
ts-extras
tiny-invariant - Throw an error if something unexpected occurs.

TypeScript with Node

  1. bun.sh - fastest, but still experimental
  2. With esbuild: esbuild-runner, tsup, esbuild-register, tsm.
  3. With swc: ts-node with swc - About as fast as esbuild, but a little more boilerplate to setup.
  4. ts-node-dev - Same as ts-node, but restarts faster

Boilerplates:
swc with Node boilerplate
https://github.com/reconbot/typescript-library-template

Generators for mocking/ testing

Books

Websites / Repos

https://github.com/mdevils/typescript-exercises

TypeScript Tips

Validate children

const allowedChildren = ["string", "span", "em", "b", "i", "strong"];

  function isSupportedElement(child: React.ReactElement) {
    return (
      allowedChildren.some((c) => c === child.type) || ReactIs.isFragment(child)
    );
  }

  // Only certain child elements are accepted. Recursively check child elements to assure all elements are supported.
  function validateChildren(children: React.ReactNode) {
    return React.Children.map(children, (child) => {
      if (!React.isValidElement(child)) return child;

      const elementChild: React.ReactElement = child;
      if (child.props.children) validateChildren(elementChild.props.children);

      if (!isSupportedElement(elementChild)) {
        throw new Error(
          `Children of type ${
            child.type
          } aren't permitted. Only the following child elements are allowed in Inline Alert: ${allowedChildren.join(
            ", "
          )}`
        );
      }
      return elementChild;
    });
  }

Map JSON strings to native JavaScript dates

Type your data as close to the source as possible, or better yet, use Zod to validate if on the server (Zod is a bit heavy for the client). Zod is especially helpful for validating and Zod schemas can easily be converted into TS types via infer. My Sandbox with a Zod format function

Strongly type env vars using Zod. And use types to disable process.env

React's ref has 2 types: React.ObjectRef (which has a current prop, and is normally what you'll want), and React.RefCallback which is for callback functions. The shorter, React.Ref is the broader type that covers both, so that's rarely useful.

Prefer unknown over any. How to read: unknown is I don’t know. any is I don’t care. So we should favor saying “I don’t know”, rather than “I don’t care”. Because “I don’t know” means, when you work with this, you need to narrow the type.

Why I don't use React.FC

"Extend" without an interface using an intersection type. But AVOID THIS because it's slow:

export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  /** Adds a class to the root element of the component */
  className?: string;
}

Strongly typed keys via Object.keys

// Helper to get strongly typed keys from object via https://stackoverflow.com/questions/52856496/typescript-object-keys-return-string
const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;

Default Props and destructuring

type LoginMsgProps = {
  name?: string;
};

function LoginMsg({ name = "Guest" }: LoginMsgProps) {
  return <p>Logged in as {name}</p>;
}

WithChildren helper type:

type WithChildren<T = {}> = 
  T & { children?: React.ReactNode };

type CardProps = WithChildren<{
  title: string;
}>;

Clone React.ReactNode

function getIcon() {
  if (React.isValidElement(icon)) {
    React.cloneElement(icon, { className: "extra-class"});
  }
}

Support spreading props or using a computed property

[x: string]: any;

Use a type to declare a union type

Given a type like this

export const icons = {
  arrowDown: {
    label: "Down Arrow",
    data() {
      return <path ... />
    }
  },

  arrowLeft: {
    label: "Left Arrow",
    data() {
      return <path ... />
    }
  },

  ...
}

You can declare a type like this:

export type IconName = keyof typeof icons

Think of this as "whenever something has the type "IconName", it must be a string that matches one of the keys of the icons object." More on why this works.

Declare a prop on a native HTML element as required

First, create a helper:

type MakeRequired<T, K extends keyof T> = Omit<T, K> &
  Required<{ [P in K]: T[P] }>;

Then, use it:

type ImgProps 
  = MakeRequired<
    JSX.IntrinsicElements["img"], 
    "alt" | "src"
  >;

export function Img({ alt, ...allProps }: ImgProps) {
  return <img alt={alt} {...allProps} />;
}

const zz = <Img alt="..." src="..." />;

Remove a prop and declare it differently

type ControlledProps =
  Omit<JSX.IntrinsicElements["input"], "value"> & {
    value?: string;
  };

Spread attributes to HTML elements

type ButtonProps = JSX.IntrinsicElements["button"];

function Button({ ...allProps }: ButtonProps) {
  return <button {...allProps} />;
}

Omit a type

type ButtonProps =
  Omit<JSX.IntrinsicElements["button"], "type">;

function Button({ ...allProps }: ButtonProps) {
  return <button type="button" {...allProps} />;
}

// 💥 This breaks, as we omitted type
const z = <Button type="button">Hi</Button>; 

Conditional React Props (Use TS to only allow specific combinations of related props) - And an excellent video

interface CommonProps {
  children: React.ReactNode

  // ...other props that always exist
}

type TruncateProps =
  | { truncate?: false; showExpanded?: never }
  | { truncate: true; showExpanded?: boolean }

type Props = CommonProps & TruncateProps

const Text = ({ children, showExpanded, truncate }: Props) => {
  // Both truncate & showExpanded will be of
  // the type `boolean | undefined`
}

Single onChange handler

  const onUserChange = <P extends keyof User>(prop: P, value: User[P]) => {
    setUser({ ...user, [prop]: value });
  };

Derive array from union

const list = ['a', 'b', 'c'] as const; 
type NeededUnionType = typeof list[number]; // 'a'|'b'|'c';

Testing via Jest

Test utils:

// IMPORTANT: Import this file BEFORE the thing you want to mock.
import * as fetchModule from "node-fetch";

jest.mock("node-fetch");
const fetchModuleDefault = fetchModule.default as unknown as jest.Mock<any>;

// Disables console logging. Useful for tests that call
// code that outputs to the console, so we don't litter the test
// output with needless console statements.
export function disableConsoleLog() {
  jest.spyOn(console, "warn").mockImplementation();
  jest.spyOn(console, "info").mockImplementation();
  jest.spyOn(console, "log").mockImplementation();
  jest.spyOn(console, "error").mockImplementation();
}

beforeEach(() => {
  fetchModuleDefault.mockClear();
});

afterEach(() => {
  jest.restoreAllMocks();
});

// Useful for complete control
export const mockFetch = (fn: any = () => null) => {
  fetchModuleDefault.mockImplementation(jest.fn(fn));
  return fetchModuleDefault;
};

type MockFetchResponse = {
  responseJson?: any;
  status?: number;
};

// Convenient when you just want to specify the response
export const mockFetchResponse = ({
  responseJson = "",
  status = 200,
}: MockFetchResponse) => {
  return mockFetch(async () => ({
    ok: status >= 200 && status < 300,
    status,
    json: async () => responseJson,
  }));
};

// Via https://instil.co/blog/typescript-testing-tips-mocking-functions-with-jest/
export function mockFunction<T extends (...args: any[]) => any>(
  fn: T
): jest.MockedFunction<T> {
  return fn as jest.MockedFunction<T>;
}

Template literal types - Useful when you want to use a template string with some placeholders to declare a union of string literal types. Yes, you could generate the list manually (and you should if the list is huge), but this can be handy when the size is reasonable.

Advanced TypeScript

The pivotal moment of transitioning from intermediate to advanced TypeScript was realizing the type system is a programming language in itself, w/ variables, functions, conditionals, & loops. Utility types like Required, Record, Pick, Omit are all built with these primitives.

Generics parameterize types like functions parameterize value. Generics overview from Matt

Screen Shot 2022-09-01 at 8 11 50 AM

This table is a summary of Type Programming

Operation JS TS
Variable var/const Generic
Conditional == Conditional types - extends with ternary
List Array Union Types
Prop access obj.prop or obj["prop"] type["prop"]
Map obj.map [K in keyof T]
Filter obj.filter as with never
Pattern matching regex infer
Loops forEach, recursion, map recursively call type
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant