Posts tagged "typescript"

Storybook with create-react-app and Typescript

# generate a new app using create-react-app
yarn create react-app my-app --typescript
cd my-app

# install storybook cli
yarn add -D @storybook/cli

# initialize with storybook cli
yarn sb init

yarn sb init should not be called with the --type react flag. Without --type, the cli figures out the correct type on its own.

The correct type for an app created with create-react-app is react_scripts.

Check out the source code for available types.


Typescript generic with union

Click here to see the initial setup
enum MessageTypes {
  Success,
  DuplicateEmail,
}

interface ContentMapping {
  [MessageTypes.Success]: {
    data: any;
  };
  [MessageTypes.DuplicateEmail]: {
    email: string;
  };
}

interface Message<Type extends MessageTypes> {
  type: Type;
  content: ContentMapping[Type];
}

We'll compare the following two types:

type Option1 = Message<MessageTypes.DuplicateEmail | MessageTypes.Success>;

type Option2 =
  | Message<MessageTypes.DuplicateEmail>
  | Message<MessageTypes.Success>;
Let's substitute Message and ContentMapping
type Option1 = {
  type: MessageTypes.DuplicateEmail | MessageTypes.Success;
  content:
    | {
        data: any;
      }
    | {
        email: string;
      };
};

type Option2 =
  | {
      type: MessageTypes.DuplicateEmail;
      content: {
        email: string;
      };
    }
  | {
      type: MessageTypes.Success;
      content: {
        data: any;
      };
    };

In Option1, type and content are not connected. A malformed Message can be created:

const message: Option1 = {
  type: MessageTypes.DuplicateEmail,
  content: {
    data: "This should not be allowed!",
  },
};

On the other hand, Option2 throws an error:

const message2: Option2 = {
  type: MessageTypes.DuplicateEmail,
  content: {
    data: "This *is* not allowed!",
  },
};

Property 'email' is missing in type '{ data: string; }' but required in type '{ email: string; }'.

Check out this TypeScript playground for a live demo.


Typescript module declarations are ignored

You can write custom type definitions for a module in .d.ts files, for example:

// some-name.d.ts

// my-module will have the type `any`
declare module "my-module";

// specify the exports
declare module "my-other-module" {
  export function foo(bar: number): string;
}

.d.ts files should be in the src directory.

With tsc, this file is included globally.

Starting with version 7 of ts-node, .d.ts files are not included automatically.

Injecting TS_NODE_FILES=true tells ts-node to include the files as well.

See here for more information.


Typescript function parameter type

A previous post mentioned the Typescript ReturnType utility type. It is really handy to retrieve the return type of a function.

But there is also a type to retrieve the parameter list of a function. Meet Parameters<T>.

const add = (first: number, second: number) => first + second;

const cache = <T extends (...args: any) => any>(func: T) => {
  const cacheObject = {};
  return (...args: Parameters<T>) => {
    const hashedArgs = JSON.stringify(args);
    if (cacheObject.hasOwnProperty(hashedArgs)) {
      return cacheObject[hashedArgs];
    } else {
      return (cacheObject[hashedArgs] = func(...args));
    }
  };
};

See the full example here.

The code can be found at lib/lib.es5.d.ts:

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

Enhance Redux props with ReturnType

The following example is built in the offical Redux documentation:

import { AppState } from './store'

import { SystemState } from './store/system/types'

import { ChatState } from './store/chat/types'

interface AppProps {
  chat: ChatState
  system: SystemState
}

const mapStateToProps = (state: AppState) => ({
  system: state.system,
  chat: state.chat
})

Here, SystemState and ChatState are imported and used manually.

But as AppState is correctly typed, we can use our beloved ReturnType instead:

import { AppState } from './store'

type AppProps = ReturnType<typeof mapStateToProps>

const mapStateToProps = (state: AppState) => ({
  system: state.system,
  chat: state.chat
})

Check out this CodeSandbox for a runnning example.


Get Pull Request Approval with the GitHub API (v4)

Natively, the GitHub API does not provide a way to obtain a pull request's approval status. Here's a workaround.

It is necessary to compare the date of the newest commit and the date of last approval, because new commits automatically invalidate any approvals (default behavior, can be configured).

import { graphql } from "@octokit/graphql"
import { Repository, PullRequest } from "./types"

const query = graphql.defaults({
  headers: {
    authorization: `token ${process.env.GITHUB_TOKEN}`,
  },
})

function isApproved(pr: PullRequest): Boolean {
  if (!pr.reviews.edges.length) return false

  const latestCommit = new Date(pr.commits.edges[0].node.commit.authoredDate)
  const latestApproval = new Date(pr.reviews.edges[0].node.updatedAt)

  return latestApproval > latestCommit
}

async function getAllApprovedPullRequests(): Promise<PullRequest[] | null> {
  const queryResult: any = await query(`{
      repository(owner: "cybertec-postgresql", name: "today-i-learned-content") {
        pullRequests(last: 25, states: OPEN) {
          edges {
            node {
              title
              number
              reviews(states: APPROVED, last: 1) {
                edges {
                  node {
                    updatedAt
                  }
                }
              }
              commits(last: 1) {
                edges {
                  node {
                    commit {
                      authoredDate
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  `)

  const repo: Repository = queryResult.repository

  if (!repo.pullRequests.edges) return null

  let pullRequests: PullRequest[] = repo.pullRequests.edges
    .map(edge => edge.node)
    .filter(pr => isApproved(pr))

  return pullRequests
}

Typescript json validation with io-ts

There's a Typescript library called io-ts that can help to strong-type json data fetched from the server and at the same time provide static typescript typing.

Problem

Imagine we have this line that fetches some data from an endpoint:

const employee = await fetchEmployee();

employee will probably have the any type. If we knew the shape of the employee object, we could create a type and cast employee:

type Employee {
  firstName: string;
  lastName: string;
}

const employee = await fetchEmployee() as Employee;

But now we are assuming the shape of employee, and if it changes in future versions of the backend, it can lead to annoying runtime errors such as accessing properties on undefined objects, which can be hard to track. We could validate it with a lib such as ajv, but we wouldn't be able to have a single source of truth.

Solution

With io-ts we can define a type like this:

import * as t from 'io-ts';

// The runtime type we will use to validate the data fetched from the server
const Employee = t.type({
  firstName: t.string,
  lastName: t.string,
});

// The static type. The above runtime type acts as the single source of truth
type Employee = t.TypeOf<typeof Employee>;

// Now we can do this (in pseudo-code)
const employee = Employee.decode(await fetchEmployee());

console.log(employee.firstName);  // works
console.log(employee.foo);        // typescript compile error

Now employee is correctly typed, and the shape of the data is validated at runtime.

Implementing the pseudo-code

The lib is great, but the documentation found in the repository's README is a little confusing. The easiest way I found to just validate data and throw if the shape is incorrect is like this:

import { getOrElse } from "fp-ts/lib/Either";  // io-ts has fp-ts as a peer dependency
import { failure } from "io-ts/lib/PathReporter";

const toError = (errors: any) => new Error(failure(errors).join('\n'));
const employee = getOrElse(toError)(Employee.decode(await fetchEmployee()));

if (employee instanceof Error) {
  throw employee;
}

console.log('the first name is', employee.firstName)

This steps can be easily extracted to a helper function.


Typescript ReturnType

Typescript includes several useful utility types to enhance the type declarations of your code-base.

The ReturnType function is one of my favorite ones, as it helps reduce type definition duplication.

Suppose you have the following function definition:

type IsInText = (
  text: string
) => (
  term: string,
  minCount: number,
  maxCount?: number,
  caseSensitive?: boolean
) => boolean

Now we want to write a function allTermsInText, that takes the function returned by isInText as an argument. It should be used like:

allTermsInText(["Typescript", "awesome"], isInText("Typescript is awesome!"))

Here is the definition without the utility type:

type AllTermsInText = (
  terms: string[],
  search: (
    term: string,
    minCount: number,
    maxCount?: number,
    caseSensitive?: boolean
  ) => boolean
) => boolean

And here the same function definition, but using ReturnType for the parameters:

let AllTermsInText = (terms: string[], search: ReturnType<IsInText>) => {
  return !terms.find(term => !search(term, 1))
}