5 Typescript tips you NEED to know
Chapters8
Explains how TypeScript widens types by default and introduces literal types and as const to preserve exact values.
A practical quick-start to TypeScript tips like as const, discriminated unions, and the powerful satisfies operator to keep types precise and safe.
Summary
Developedbyed’s quickfire TypeScript tips lean on practical examples, showing how TypeScript widens types by default and how to fight that with as const. He demonstrates narrowing literals for API methods, uses discriminated unions to keep state shapes safe, and explains why satisfies helps preserve exactness when deriving types from objects. The episode also covers typing patterns like string-interpolated CSS values, enforcing runtime-safe API endpoints, and the gotchas of optional chaining versus guaranteed properties. Ed emphasizes real-world usage—config routes, API states, and UI button configs—pulling from concrete code snippets to illustrate how to maintain precise, helpful types without losing flexibility. The talk closes with reminders about common pitfalls (empty object annotations, nullish handling) and a teaser about future experiments with Arch Linux, WSL2, and more. Overall, a concise, hands-on refresher that makes TypeScript feel less intimidating and more empowering for day-to-day coding.
Key Takeaways
- Using as const narrows a value to its exact literal type, enabling precise method strings like get or post and preventing bonanza or other invalid values.
- Discriminated unions split shared properties into distinct interfaces (loading, success, error) so code can safely narrow state-based logic without accessing irrelevant fields.
- The satisfies operator preserves exact property shapes when deriving types from objects, helping you catch typos and maintain literal unions instead of broad string-based maps.
- Type patterns like inline CSS values (e.g., number | unit) let you enforce valid string patterns at the type level, preventing invalid units at compile time.
- Optional chaining can lead to undefined returns; handling the existence of nested properties beforehand ensures TypeScript narrows to string (not string | undefined).
- Useful TypeScript utility ideas demonstrated include required, partial, omit, and pick to shape large interfaces for specific usage contexts.
Who Is This For?
Frontend and TypeScript developers who want concrete patterns to keep types precise in real-world apps—APIs, UI components, and state management. Great for folks who've found TS widening frustrating and are looking for practical rules of thumb.
Notable Quotes
"This makes it a read-only, so it will actually just get the exact literal type here."
—Demonstrating how as const narrows a value to a literal type.
"Now I just switch this to type and set it equal to error state, loading state, and success state."
—Introducing discriminated unions to safely model API state.
"Satisfies catches that. If you remove this and say satisfies instead now you're going to see it gets highlighted."
—Highlighting the practical benefit of the satisfies operator for type derivation and protection.
"If you misspell this and do endpoints.do.users that doesn't exist. It's undefined at runtime."
—Illustrating how type annotations alone offer less protection than with satisfies or precise literals.
"Optional chaining can lead to undefined returns; handle the existence of nested properties beforehand."
—Cautioning about common beginner mistakes with optional chaining.
Questions This Video Answers
- How do I use as const to preserve literal types in TypeScript?
- What are discriminated unions and how do I implement them for API state in TS?
- How does the satisfies operator help with deriving types from objects in TypeScript?
- What are common TypeScript gotchas when annotating objects and why should I avoid empty object types?
- How can I enforce string patterns and CSS-like values in TypeScript types?
TypeScriptas constDiscriminated unionsSatisfiesLiteral typesCSS value typingAPI state typingUtility typesOptional chainingType narrowing
Full Transcript
Hey there, my gorgeous friends on the internet. Merry Christmas. Hope you got gifted some nice socks this season around. Uh, I got two sets. How about that? I wanted to make a little episode here talking about Typescript cuz I just like just reflecting back over the years made me realize how much I was struggling learning TypeScript just because I was fighting the type system so much. So, I added a couple of things here that hopefully will make that journey a bit easier for you. And without further ado, let's get into it. One really important aspect of Typescript is that it tries to widen your types wherever possible.
And it does this to essentially give you flexibility to mutate variables. But you might have some problems with this. So let me show you. So we have a request object here and let's say we have a method of type get and we also have maybe a URL. Okay, like an API endpoint here. So API/ users. Okay. And then let's say we want to invoke this. So let's see we have a function that does this. So you have function make request. Okay. And this takes in a URL which is a type string. And we also have a method.
But let's say the method here uh is only of type get or of type post. So it wouldn't accept anything else. Uh and this is what you call a literal type. This is more narrow down than what Typescript does by default here. For now I'll just return nothing. It doesn't really matter what we return here. So, let's just do an empty strings. Uh, but check this out. If I do a make request and I pass the URL, so I can say request URL and then I can say request methods, we are going to get an error.
It's going to say argument of type string is not assignable to parameter of type get or post. So, this is the literal type. It's the exact string that we defined in here. Whereas here by default TypeScript widens this and it says well it can be any string right because it is like in JavaScript I can mutate this even though it's a const I can still come here and say request do method equals to bonanza that's valid JavaScript um but what can we do here then how can we make this so it actually catches this and it only accepts these two specific get and posts Well, what we can do is we can simply add as const here and this makes it a uh only a readable.
So if read only only a readable u makes it read only. So it will actually just get the exact literal type here. So we're essentially narrowing it down more now. So now as you can see we have zero errors here. Awesome. Good job. Now, if I come here and say post, that still works. If I come here and say bonanza, that catches it. As you can see, it errors out. It says argument of type bonanza is not assignable. Okay, so that's great because the worst thing that you could probably have is that for the method here, you don't have those literal types and you just have string and then you can pass anything down here for the method and then nothing is complaining here.
See, you can pass in whatever you want. Uh so make sure that when you want to pass down exact values is that you are aware that TypeScript just tries to widen these types by default. One thing I really like about as const is that you can derive the types really easily and this is great for config files or if you're doing routes or something. Uh so in this case I have routes here and as you can see home about users all strings right and if I want to do a type of here of the routes I basically get string but if I come here and say as const I make this read only you can see it here.
So if I do a type of routes and I pass in the key here to essentially look at the values now I get a nice union type between these. Next up let's look at discriminated unions. This is a really important pattern that you can use uh that essentially allows you to share multiple types uh that share a common property. This comes really handy when you're building out APIs or maybe a library and you're setting up the structure and you want it to be nice and flexible and also easy to understand. Uh so let me kind of show you what you might run into as a problem.
Uh so let's say we're building a state management library and we have an API state here. So you might be inclined to do something like this. We define the interface and we might have a status here, right? Are we loading right now? Are we erroring right now? Or we are successing. Okay. So we might have these three different states and then you might be inclined to do something like this where we define an optional data or we might define an optional error. Uh but this is not too good to do and you'll see why. So I'll put this as a string.
Uh and then let me also do a data here. Uh let's say we have a type for this. Let's say user for now. We have a name of Ed. Okay. So we go down here past user. Okay. So that's how it looks like. That's our API state. Now the problem is here when we define a loading state this is a type of API state is that I can select status here loading that's great but I have data and error here whereas I don't really need these two values in the loading state it doesn't really make any sense to have these here uh so my API state is flexible but it's going to give me a hard time actually narrowing ing down the types and what I have available.
Uh, and that's especially evident when maybe I have a function that I want to maybe render out some JSX based on the state I have or maybe some data based on the state. So I might have something like this render state and I can pass in a state that's a type of this API state and I can do a switch statement where I check the status. So state.status and check this out. Open this up. If I have a case of loading, let's return. Well, what can I return here? State dot is there something in data error status.
Like I don't need all of these. I just need status here. Um and if I go down here, I can do another case of success. Right? I'm going to have basically everything offered here for me that's in my interface and that's not too good. It'll make it more complicated for you to understand what you have available at certain parts in your code. So state dot again it's just going to give you everything because it it it will have no no way to narrow any of this down. So rather than doing this what we can do is create a discriminated union.
Uh so rather than defining it like this what we can do is create an interface called maybe loading state and I can just define the status on here status loading and that's it. I don't need anything else on this loading state. Then I have an interface maybe of success. And here I can set the status to success. And this is the discriminant, right? This is what a discriminated union is. This is uh the property that gets shared with the different types. But here I can add data if I want to. Right? I can say data is a type of user.
And then I can also add that's it. I wouldn't need to add error here. And then I can have another one interface error state. And then I can say status error. And then I can go down here and say data. No, I guess no data, but error uh is a type of string. Cool. So now I just switch this to type and set it equal to error state, loading state, and success state. So I'm just combining all of them. And now if I go down here in my loading, check this out. I should only have access to status and nothing else.
It makes no sense to access data or error. And for the success state as well, if I check here, I have access to data and status but no error. So it's really nice. You can also define it like this if you want to. You can just do directly on one rather than doing three different interfaces. You can just say type payment for example and you have a method of credit card. Now credit card is going to take in different things. So card number, CVV, expiry whereas PayPal might only take in the email address and there when you actually do the process payment in your switch statement now you can narrow everything down and payment uh will only give you here anything that's related to the car credit card, right?
It's not going to give you email for example. that should only be available uh down here, right? So I wouldn't be able to access the card CVV. So So if I do payment dot, as you can see, we have only the email and the method provided for you. So it's really powerful. One limitation with type annotations is by default it widens the types that it accepts and often loses these specifics. So let's say you're building a UI library and you have a button config where it can be a variant of primary, secondary and muted and the size can be small or default.
So let's say we define a button and we pass a type annotation here. So it's a button config. So we set that equal to and yes this works. We get variant here and we can choose primary. We get size as well. We can pick default for example. But the problem is is we lose the specifics down here. So if I do button variant and I want to see if this is primary, I open this up and it says, well, it can be any of these three. It can be primary, secondary or muted. However, what if I want to know what exactly this button is?
Well, because you might have two here, right? You have a button two. Let me just copy paste this actually. Right? You might have something like that. And this is actually a secondary one instead. Let's just call this button two. Again, same problem, right? Button two dot variant. This is going to be showing all and this is going to be showing all as well. So this is where satisfies comes in. It keeps those specifics for you. So if I say as sorry not as satisfies button config and then we can remove this type annotation from here like that and let's do the same for this one pass satisfies button config.
Now we can narrow down and keep the specifics of this. So now this says primary and now this says secondary. One thing that you need to realize about type annotations is that you're not doing any protection against types. So here we have a record which just basically says uh it should accept a key and value pair that are both strings. Okay, so string and string string and string. And if I do endpoints do users, uh this is only going to give us a string, right? Type annotations again are going to try to widen this as much as possible for you.
But we get no protection with types. So if I misspell this and do endpoints do users that doesn't exist. It's undefined at runtime. You have no protection against this and it's perfectly fine. And satisfies actually catches that. So if you remove this and say satisfies instead now you're going to see it gets highlighted and it says that users not does not exist on this specific type. So you get that protection and you also get the actual literal here. This becomes useful when you want to derive types as well. So let's say we have a record here that's string boolean.
So we have dark mode that's the string boolean true and beta features here that's set to false. If I want to do a key of type of flags here if we check the type of this it's just going to be string. It's useless. That doesn't help me at all. However, if we use satisfies here, then we actually get uh a literal between dark mode and beta features. Did you know you can also enforce string patterns at a type level? This is really cool and could be really helpful. So, let's say you have a CSS unit here and this could be maybe RAM or or a pixel, right?
Or M, etc., etc. Cool. Now, check this out. I can do a type of CSS value and I'm just going to interpolate it in strings here. So I can say it's going to be a number and here at the end it's going to be a CSS unit. So I'm essentially combining these two together. So let's say you have a function called something like set width that takes in a value that's going to be a type of CSS value. And check it out. If I hover over this, look, it's going to create a union and it's going to say number ram number pixel number m.
Uh the implementation detail here doesn't matter. I can just return value. But if I try to do something like this, set width 20, look at that. It's not assignable. However, if I do ram, it works. And it even works if I do pixels or m. So really, really cool. This is really helpful when you're building APIs as well. So you can just define your types here forget, post, put, delete, the version of the API and resources and then you can interpolate it like this. So you can just create an API endpoint and pass that down here.
And if we look at this, look, it's it just creates all of them for you automatically. So if we look at this, this is available. However, this is not assignable because it doesn't exist. We don't have V3, only V1 and V2. And finally, here are a couple of gotchas that you should be aware of when you have an object. And if you define the keys as one, two, these are going to be strings, not numbers. Okay? So, make sure if I do object keys here of object. So, this is going to be the one and the two, it's going to be string.
Also remember these four keywords because they can come in really handy like required, partial, emit, and pick. So have if you have an interface here user but maybe one specific instance you actually do require them to have all the properties added. So even though we mark this email as optional you can add the required keyword and pass user in here as a generic and then all the properties will become uh required. So as you can see email is not marked as optional anymore. And then you can use partial for example if you only want to get a couple of things out here.
uh you have omit so you can take out maybe a specific thing like ID here. So now we're only going to have name and email and then you can also uh pick a specific property from uh this large interface because you can imagine this can be yeah it's small now we have ID name and email but you can imagine it can be a really large interface so you can just pick out the values that you require. So, one thing that tripped me up a lot about TypeScript is when I would type annotate an object with uh an empty curly brackets like this.
And when you call it, look at that. You can pass it down like that with the curly brackets, empty object, but it works with a name as well, but also just a string and also a number. That works as well. You only start getting problems when you do null or undefined. That actually stops you and it says, "Hey, don't do that." So just keep in mind when you do something like this, TypeScript doesn't look look at this as an empty object. It just looks at it as u a value that's non-nullable uh non-nullish or undefined.
So to fix this simply use a record of string and never and then that'll stop you from doing anything like that. Uh if I go down here, let me just comment this out. Look at that. It actually stops you from calling process empty with a string. It says argument of type string is not assignable to this specific record. Now, there's a bunch and a bunch more. If you want me to do a part two of this episode, feel free to do it. But the last one I want to show you is when you do optional chaining, you might expect to get a value, but you actually get undefined.
So, here we have an interface with address uh which is optional. And then on this we also have a city that's string. Now when we call get city here and pass the user in we do user.add address.c city this is not going to give us string. It's going to give us string or undefined because address here is optional in the end. So this is a more of a beginner mistake. Uh but you need to basically handle the case beforehand. So you do something like if user address exists then you can run this uh just fine.
So if I put that there I put that there. There we go. We don't have the error anymore. Uh because we are handling the case. We don't even need the question mark there anymore. And now here city is 100% only a string. All right there we go. Hope you enjoyed this little episode. Make sure you drop a little like and a subscribe. And I'm gonna start nuking my Arch Linux setup here and start experimenting on some stuff. Expect videos on this. I'm going to try out the Windows with WSL 2. See how that experiences and the Mac Mini as well.
And then I'll pick one or I'll end up coming back to Arch Linux. We'll see. Uh, hope you enjoy the rest of your day and I'll see you
More from developedbyed
Get daily recaps from
developedbyed
AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.









