TypeScript: Infer Types to Avoid Explicit Types

Nick Taylor - Nov 21 '23 - - Dev Community

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

feat: Svelte added to interests dropdown list #2168

Description

added svelte to interests dropdown list

What type of PR is this? (check all applicable)

  • [x] 🍕 Feature
  • [ ] 🐛 Bug Fix
  • [ ] 📝 Documentation Update
  • [ ] 🎨 Style
  • [ ] 🧑‍💻 Code Refactor
  • [ ] 🔥 Performance Improvements
  • [ ] ✅ Test
  • [ ] 🤖 Build
  • [ ] 🔁 CI
  • [ ] 📦 Chore (Release)
  • [ ] ⏩ Revert

Related Tickets & Documents

Fixes #2167

Mobile & Desktop Screenshots/Recordings

Added tests?

  • [ ] 👍 yes
  • [x] 🙅 no, because they aren't needed
  • [ ] 🙋 no, because I need help

Added to documentation?

  • [ ] 📜 README.md
  • [ ] 📓 docs.opensauced.pizza
  • [ ] 🍕 dev.to/opensauced
  • [ ] 📕 storybook
  • [ ] 🙅 no documentation needed

[optional] Are there any post-deployment tasks we need to perform?

[optional] What gif best describes this PR or how it makes you feel?

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

fix: improved type inference of interests and recommendations #2192

Description

What type of PR is this? (check all applicable)

  • [ ] 🍕 Feature
  • [ ] 🐛 Bug Fix
  • [ ] 📝 Documentation Update
  • [ ] 🎨 Style
  • [ ] 🧑‍💻 Code Refactor
  • [ ] 🔥 Performance Improvements
  • [ ] ✅ Test
  • [ ] 🤖 Build
  • [ ] 🔁 CI
  • [ ] 📦 Chore (Release)
  • [ ] ⏩ Revert

Related Tickets & Documents

Mobile & Desktop Screenshots/Recordings

Added tests?

  • [ ] 👍 yes
  • [ ] 🙅 no, because they aren't needed
  • [ ] 🙋 no, because I need help

Added to documentation?

  • [ ] 📜 README.md
  • [ ] 📓 docs.opensauced.pizza
  • [ ] 🍕 dev.to/opensauced
  • [ ] 📕 storybook
  • [ ] 🙅 no documentation needed

[optional] Are there any post-deployment tasks we need to perform?

[optional] What gif best describes this PR or how it makes you feel?

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;
}
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

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<'."/>

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

Bug: Svelte thumbnail missing in pills #2195

Describe the bug

When onboarding and selecting Svelte as an interests, the svg is missing on the pill/badge. We added the svg to the topic-thumbnails directory, but this needs to be added to the img/icons/interests directory in an interesting format. It appears to be using an svg sprite with an image data uri when I compared the react svgs. Here are the 2 file types. I would attempt this, but I'm not sure how to get the format needed for that interests svg. I assume you just need to take the Svelte svg that was added and run it through some tool, but I don't know which one. interests thumbnail

Steps to reproduce

  1. Onboard or add Svelte as an interest
  2. See svelte Svelte instead of the svg

Browsers

No response

Additional context (Is this in dev or production?)

No response

Code of Conduct

  • [X] I agree to follow this project's Code of Conduct

Contributing Docs

  • [X] I agree to follow this project's Contribution Docs

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .