TypeScript: Infer Types to Avoid Explicit Types
Nick Taylor
Posted on November 21, 2023
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.
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.
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;
}
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.
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
- Onboard or add Svelte as an interest
- 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();
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.
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
Posted on November 21, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2023