Skip to content
Stop Making Your TypeScript Types So Broad

Stop Making Your TypeScript Types So Broad

Published: at 05:45 AM

Some see TypeScript as a necessary evil.

The new linter warnings you have to satisfy, another gotcha to make your coding complex.

I used to think the same.

Then I realized bad types were the real problem.

I think TypeScript is incredible, and it has become my preferred language since I tried it.

Added complexity?

Yes, but the clarity it brings to the code is well worth it.

However, if you only see TypeScript as a roadblock and you’re working only on satisfying the compiler, you can end up with:

I'd like to share an example from some code I recently reviewed.

This structure appears fine at first glance, but the type definition ultimately did more harm than good.

const objectA = {
    totalWaitTime: {
        chartData: {
            name: "Total Wait Time",
            children: [
                { name: "Stage 1", value: 5 },
                { name: "Stage 2", value: 8 },
            ],
        },
        stats: {
            hasWaitTime: true,
        },
    },
    totalWorkTime: {
        chartData: {
            name: "Total Work Time",
            children: [
                { name: "Stage 1", value: 15 },
                { name: "Stage 2", value: 12 },
            ],
        },
        stats: {
            hasWorkTime: true,
        },
    },
    totalTime: {
        chartData: [
            {
                key: "Stage 1",
                values: [
                    { x: "2023-01-01", y: 20 },
                    { x: "2023-01-02", y: 22 },
                ],
            },
            {
                key: "Stage 2",
                values: [
                    { x: "2023-01-01", y: 18 },
                    { x: "2023-01-02", y: 21 },
                ],
            },
        ],
        stats: {
            hasTotalTime: true,
        },
    },
};

How I’d type this if my only goal was to make the TypeScript compiler happy?

I could take a top-down approach and look at what’s possible:

  1. The object has keys that are strings and some chart data. I’d write this as const objectA: Record<string, QueueTimeChartResult> = {

  2. Then the values have a chartData and a stats key, where the values are as follows:

    1. chartData contains a name and an array of children or key and an array of values

    2. stats contains either hasWaitTime, hasWorkTime, or hasTotalTime

Then I’d arrive at something like this:

interface QueueTimeChartData {
  key?: string;
  name?: string;
  children?: { name: string, value?: number }[];
  values?: {x: string, y: number}[];
}

export interface QueueTimeStats {
    hasWaitTime?: boolean;
    hasWorkTime?: boolean;
    hasTotalTime?: boolean;
}

export interface QueueTimeChartResult {
    chartData: QueueTimeChartData | QueueTimeChartData[];
    stats: QueueTimeStats;
}

You can try this version out for yourself in the following TypeScript Playground link.

But what’s wrong with this?

I added a type, but the structure isn’t clearer, but more confusing.

Why?

Now it’s documented in QueueTimeStats we can have an object like this:

stats: {
  hasWaitTime: false;
  hasWorkTime: true;
  hasTotalTime: true;
}

Which is never the case.

While this is somewhat better than using any, the thing is…

TypeScript isn’t just a static checker — it’s documentation for your code.

An additional way to convey information about our code, and if your types resemble the above, this can be confusing.

How can we fix this?

By using types that aren’t so forgiving.

In our case, the top-level object seems to hold the same structure, but if you look deeper, chartData has two forms, so I’d start from there:

export interface ChartData {
    name: string;
    children?: { name: string, value?: number }[];
}

export interface PositionChartData {
    key?: string;
    values?: {x: string, y: number}[];
}

I just named the second structure PositionChartData because the x and y tell me this is some kind of position on the chart (where probably the X axis is the time and the Y axis is the value).

If I expand further from chartData, I can see that for each of the top-level keys, totalWaitTime, totalWorkTime, and totalTime, the key inside stats is different and is related to the top-level key.

This is why I have decided to create structures describing these top-level objects:

export interface TotalWaitTimeChartData {
    chartData: ChartData;
    stats: {
        hasWaitTime: boolean;
    }
}

export interface TotalWorkTimeChartData {
    chartData: ChartData;
    stats: {
        hasWorkTime: boolean;
    }
}

export type TotalTimeChartData {
    chartData: PositionChartData[];
    stats: {
        hasTotalTime: boolean;
    }
}

This version tells a clearer story, one where each object means exactly what it should.

Now it’s clear that you’ll have either:

stats: {
  chartData: [...]
  hasTotalTime: boolean;
};

or

stats: {
  chartData: {...}
  hasWorkTime: boolean;
};

but never

{
  chartData: [{
    name: "Stage 1",
    value: 5,
    key: "stg-1",
    values: [{
      x: "2023-01-01",
      y: 6
    }]
  }],
  stats: {
      hasTotalTime: false,
      hasWorkTime: true
  }
}

You can find the updated TypeScript Playground here.

Conclusion

How to avoid typing objects too broadly, as was done first in this case?

As I write this, I think what made a difference in my approach was starting with the inspection of the object’s deepest structure, then moving towards the top-level object.

While the original type first looked at the top-level object, it made a type that can hold anything that can appear there.

You can call this bottom-up vs. top-down typing – I don’t know if this is actually a term.

But the bottom line is:

Broad types make the code pass, narrow types make it clear.

Generative AI with React JS: Build and Deploy Powerful AI Apps

Learn how to leverage the OpenAI API in React to create advanced generative AI applications. Throughout the book, you'll cover topics like generating text, speech post-processing, building a social media companion app, and deploying the final application.

Learn More
Generative AI with React JS: Build and Deploy Powerful AI Apps

What to Read Next