TypeScript: Infer Types to Avoid Explicit Types

TypeScript: Infer Types to Avoid Explicit Types

The idea for this post came about while I was reviewing this pull request (PR) for OpenSauced.

My friend Brittney Postma (@brittneypostma) who is a huge Svelte fan, wanted to add Svelte to the list of available interests from our explore page.

Image description

She made some changes which worked while running the dev server, but TypeScript was complaining, causing the build to fail.

4:49:27 PM: ./lib/utils/recommendations.ts:3:7
4:49:27 PM: Type error: Property "svelte" is missing in type "{ react: string[]; javascript:
stringIl; python: string|]; ml: string|]; ai: stringI]; rust: string[l; ruby: string[]; c:
stringIl; cpp: string|]; csharp: string|]; php: string|]; java: string[]; typescript: string|];
golang: string||; vue: string||; kubernetes: string|]; hacktoberfest: string|]; clojure:
stringIl; }" but required in type "Record<"ruby" | "javascript" | "python" | "java" ||
"typescript" | "csharp" | "cpp" | "php" | "c" | "ai" | "ml" | "react" | "golang" | "rust" |
"svelte" | "vue" | "kubernetes" | "hacktoberfest" | "clojure", string[]>".
4:49:27 PM: 1 | import { interestsType } from "./getInterestOptions";
4:49:27 PM: 2
4:49:27 PM: > 3 | const recommendations: Record‹interestsType, string[]> = {
4:49:27 PM: ^
4:49:27 PM: 4 | react: ["Skyscanner/backpack"],
4:49:27 PM: 5 | javascript: ["EddieHubCommunity/LinkFree"],
4:49:27 PM: python: ["randovania/randovania"],
4:49:28 PM: Failed during stage "building site": Build script returned non-zero exit code: 2

I mentioned adding 'svelte' to the topic prop's union type in the LanguagePillProps interface in our LanguagePill component should resolve the issue. Narrator, it did.

Having to add 'svelte' to the topic props type resolved the issue, but it was extra work. Typically, you want to infer types as much as possible.

Just a note. This is not criticizing Brittney’s pull request (PR). This post is about a potential refactoring I noticed while reviewing her PR which could improve the types' maintenance in the project.

Examples of Type Inference

You might already be inferring types without realizing it. Here are some examples of types being inferred.

let counter = 0

counter gets inferred as type number. You could write this as let counter: number = 0, but the explicit type is unnecessary.

Let's look at an example of an array

let lotteryNumbers = [1, 34, 65, 43, 89, 56]

lotteryNumbers gets inferred as Array<number>. Again, you could explicitly type it.

// Array<number> or the shorter syntax, number[]
let lotteryNumbers: Array<number> = [1, 34, 65, 43, 89, 56]

But once again, it's unnecessary. Take it for a spin in the TypeScript playground to see for yourself.

Let’s look at a React example, since plenty of folks are using React. It’s pretty common to use useState in React. If we have a counter that resides in useState, it’ll get set up something like this.

const [counter, setCounter] = useState<number>(0);

Once again, though, we don’t need to add an explicit type. Let TypeScript infer the type. useState is a generic function, so the type looks like this useState<T>(initialValue: T)

Since our initial value was 0, T is of type number, so useState in the context of TypeScript can infer that useState is useState<number>.

The Changes

I discussed the types refactor on my live stream for anyone interested in a highlight from that stream.

And here's the PR I put up.

I did some other refactoring in the pull request, but the big chunk of it was this diff.

interface LanguagePillProps {
-  topic:
-    | "react"
-    | "javascript"
-    | "python"
-    | "ML"
-    | "AI"
-    | "rust"
-    | "ruby"
-    | "c"
-    | "cpp"
-    | "csharp"
-    | "php"
-    | "java"
-    | "typescript"
-    | "golang"
-    | "vue"
-    | "Kubernetes"
-    | "hacktoberfest"
-    | "clojure"
+  topic: InterestType
  classNames?: string;
  onClick?: () => void;
}

InterestType is a type inferred from the interests array (see getInterestOptions.ts).

const interests = [
  "javascript",
  "python",
  "java",
  "typescript",
  "csharp",
  "cpp",
  "php",
  "c",
  "ruby",
  "ai",
  "ml",
  "react",
  "golang",
  "rust",
  "svelte",
  "vue",
  "kubernetes",
  "hacktoberfest",
  "clojure",
] as const;
export type InterestType = (typeof interests)[number];

Aside from the type being inferred, the type is now data-driven. If we want to add a new language to the interests array, all places where the InterestType are used now have that new language available. If there is some code that requires all the values in that union type to be used, TypeScript will complain.

TypeScript complaining that the property 'svelte' is missing in type '{ react: any; rust: any; javascript: any; ai: any; ml: any; python: any; typescript: any; csharp: any; cpp: any; php: any; c: any; ruby: any; java: any; golang: any; vue: any; kubernetes: any; hacktoberfest: any; clojure: any; }' but required in type 'Record<"javascript" | "python" | "java" | "typescript" | "csharp" | "cpp" | "php" | "c" | "ruby" | "ai" | "ml" | "react" | "golang" | "rust" | "svelte" | "vue" | "kubernetes" | "hacktoberfest" | "clojure", StaticImageData>'.

In fact, a new issue was opened today because an SVG for Svelte was missing in another part of the application.

If the InterestType has been used everywhere, that error would have been caught by TypeScript, just like in the screenshot above.

Counter Example: Explicit Types Required

Let’s look at another React example.

const [name, setName] = useState();

We’re on the infer types hype and set up a new piece of state in our React application. We’re going to have a name that can get updated. Somewhere in the application, we call setName(someNameVariable) and all of a sudden, TypeScript is like nope! What happened? The type that gets inferred for

const [name, setName] = useState();

is undefined, so we can’t set a name to a string type. This is where an explicit type is practical.

const [name, setName] = useState<string | undefined>();

If the string | undefined, I recommend reading about union types in TypeScript.

Typing Function Return Types

For return types in functions, there are definitely two camps. Some think that return types should always be explicitly typed even if they can be inferred, and others not so much. I tend to lean towards inference for function return types, but agree with Matt Pocock's take that if you have branching in your function, e.g. if/else, switch, an explicit return type is preferred. More on that in Matt's video.

As mentioned, inferred types are the way to go for most cases, but Kyle Shevlin (@kyleshevlin) messaged me after this blog post went out with another use case to explicitly type the return type.

If a function returns a tuple, you need to explicitly type the return type. Otherwise, the inferred return type will be an array whose items have the union type of all the array items returned.

A TypeScript function returning a tuple even though the inferred type is not a tuple

You can see this in action in a TypeScript playground I made.

Wrap it up!

Types are great, and so is TypeScript, but that doesn't mean you need to type everything. Whenever possible, lean on type inference, and explicitly type when necessary.

Other places you can find me at:

🎬 YouTube
🎬 Twitch
🎬 nickyt.live
💻 GitHub
👾 My Discord
🐦 Twitter/X
🧵 Threads
🎙 My Podcast
🗞️ One Tip a Week Newsletter
🌐 My Website