DEV Community

Cover image for 4 Must-Know TypeScript Tips & Tricks
Sam Piggott
Sam Piggott

Posted on • Updated on

4 Must-Know TypeScript Tips & Tricks

TypeScript has some incredibly nifty utilities that can be used to make your codebase more readable, efficient and safer.

In this article, I've compiled a list of four of my favourite utilities that I use in my daily workflow, along with some examples and explanations of each.

They've helped my TypeScript workflow - I hope they help yours, too!

Before we get started...

If you're new to TypeScript, I have a full course for beginners available right here on my YouTube channel!

It covers all the essentials you need to get started with TypeScript as a JavaScript developer.

If that sounds like something you're looking for, check it out here - I'd love to hear your thoughts on it!

Pick and Omit

Pick and Omit are special utility types that TypeScript provides as a way to add more convenience and power when creating new types for object shapes. Let's take a look at each one in detail with some examples...

Pick

In the following example, we've constructed an interface type called Consumable, which has got a bunch of properties that relate to something you could eat or drink.

TypeScript provides the Pick utility to allow us to "pluck" properties from our object shape types, and create a new type from that. Let's create a new type, Pizza, by simply picking out the relevant properties from the Consumable type.

Nice! Let's go over that in a little more detail.

  • The first parameter that we pass into the Pick utility is the type that we want to pick from.
  • The second parameter is either a single value or a union type of all of the properties we want to pick out from the type we passed in as the first parameter.

In the above example, we're picking size and caloriesPerServing from the Consumable type to construct our brand new type, Pizza.

Let's go one step further. The cool thing about creating a new type is that we can use it just like anything else - so let's extrapolate our Pizza type and add a toppings property to our object shape...

In this example, we're declaring Pizza as an interface, so that we can extend from our new Picked type and add a brand new parameter, toppings, to it. That means that our Pizza interface, after being compiled, would have the following properties:

  • size: 'large' | 'medium' | 'small'
  • caloriesPerServing: number
  • toppings: string[]

Omit

Omit works just like Pick - but the inverse.

We pass Pick the properties we wish to pluck out from the object type, but with Omit, we pass the properties we wish to exclude from the initial object type.

Let's take a look at an example to make things a little clearer. Just like with Pick, we'll use the same Consumable type once again as a base - but this time, we'll create a new type called Sandwich.

Our Consumable type has a property on it called millilitresPerServing. That's not really relevant to a sandwich - so by using Omit, we can pass in two arguments:

  • First, the type that we wish to use as a base...
  • ...followed by a single or union type of the keys that we wish to omit from that interface.

(Just like with Pick!)

That means in this example, our Sandwich type would have the following properties:

  • size: 'large' | 'medium' | 'small'
  • caloriesPerServing: number
  • gramsPerServing: number

Notice that millilitresPerServing isn't present in that list - that's because our Sandwich type intentionally omits that from our new type by using the Omit utility as described above.

What's just as cool - just like with Pick, the previous example, we can use the new type generated by the Omit utility as a base to extend from. Let's extend our Sandwich type by adding some fillings...

Omit and Pick really come into their own in more complex applications, particularly when you have a lot of overlapping object shapes that have properties which should remain identical in type. They're a dream for composition!

Required & Partial

Just like Pick and Omit that we covered above, Required and Partial are utility types that allow us to create new types from our object types. Let's take a look into each one to see how they could be used as part of a workflow.

Required

Okay, simple example - we have an interface for a (fictional) sign-up form on a website, with all the usual suspects present.

Notice that in the above example, we've got a few ?s in there.

Those are use to indicate that those properties are optional - which means that they're allowed to be undefined. Let's create an input object using our type:

(Note: I could have also just omitted all of of the properties with undefined as a value, but I wanted this example to be a bit more explicit for easy reading!)

Let's say for example that we have another form in our web app elsewhere, which uses the same shape of input - but this time, requires that we supply values to all of the properties in our MyFormInputs object.

If we wanted to, we could just re-write that same interface again, keeping all our keys and value types the same - but removing those pesky ?s to ensure that we can't pass any undefined values in...

...but, following the classic DRY rule, this should start to leave a bit of a bad taste in your mouth. There must be a better way...

Thankfully, that's where the wonderful Required utility comes in!

Let's create a new type called MyFormInputsRequired and make all of the properties on it non-nullable.

Required simply takes one parameter - the interface or object type that we want to make all properties enforced. In the above example, we also create a new object using that interface, and ensure that every single property has a corresponding value.

If the key wasn't present in requiredInputs, or if we supplied null or undefined as any of the values, this would throw an exception at compile-time.

Nice and safe!

Partial

Partial is the exact opposite of Required - instead of making all the properties in an interface or object type required, it makes them all optional. (if you've read this entire article from the top, you're probably beginning to notice a pattern...)

Let's take a look at an example on how it could be used. We'll go back to videogames to maintain some semblance of variation...

In the above example, we've introduced our VideoGame interface, which has three properties on it which are all required.

Let's say we wanted to create a new type making all of the properties optional. We'll use the power of Partial to make this happen...

In the example above, we create a new type named VideoGamePartial, and, just like how we used Required above, we pass the Partial utility a single object type.

This creates a new type, copying the exact shape of the VideoGame interface, but making all of the properties optional.

When we create a new object using our new VideoGamePartial type (as demonstrated in the nintendoGame value at the bottom of the above example), we can see that we're able to skip two of the previously required values - description and ageRating.

Taking this to an extreme, because Partial makes all of our properties optional, it would actually be valid to use that type to simply create an empty object...

...but that's probably more of a hypothetical use-case, as I can't imagine that being super useful in day-to-day πŸ˜…

Finally, topping it all off (and attempting to drive home how cool these utilities are) - let's use our new Partial type as a base to extend from!

In the above example, we create a new type called SonyVideoGame, which extends from our VideoGame type that has a set of properties which are all optional.

We've then added a new (required!) type to it called platform. That means that all of the properties (and their respective optional states would be as follows):

  • title: string - Optional
  • description: string - Optional
  • ageRating: '3+' | '10+' | '16+' - Optional
  • platform: 'PS2' | 'PS3' | 'PS4' | 'PS5' - Required

Using composition and the power of TypeScript utilities, we've created a complex type which has a series of properties which are both optional & required. Neat, right?

Summary

And that concludes our whistle-stop tour on some of TypeScript's powerful utilities that are provided with the language. There's plenty of others that you can delve into over at the TypeScript handbook - but these four are some of my favourites.

CodeSnap Promo Image

If you're looking for more TypeScript learnings, I have a full video course on the basics of TypeScript over on my website at CodeSnap.io!

Happy TypeScript'ing!

Discussion (10)

Collapse
vetras profile image
vetras

when you Pick<Consumable, "size" | "caloriesPerServing"> doesnt it mean that renaming those properties will raise runtime errors because ... well ... strings?

Collapse
sam_piggott profile image
Sam Piggott Author • Edited on

Hey Vetras! It doesn't actually rename those properties - rather, it plucks them from the original interface and creates a new type consisting of the picked properties. So in that case, the resolved Pizza type would end up looking like:

type Pizza = {
    caloriesPerServing: number;
    size: "large" | "medium" | "small";
}
Enter fullscreen mode Exit fullscreen mode

Hope that makes sense :)

Collapse
vetras profile image
vetras

sorry :) I might have explained myself wrong.
I meant; What if you rename the size property to count, for example.
Would the Pizza be renamed to pizza.count?
Can the IDE pick that up? (pun intended 😎)

Thread Thread
sam_piggott profile image
Sam Piggott Author • Edited on

I guess if you wanted to add count to the new type, could you do something like so:

interface Pizza extends Pick<Consumable, "size" | "caloriesPerServing"> {
    count: number;
}
Enter fullscreen mode Exit fullscreen mode

That way, you'd get size, caloriesPerServing and count as properties on the Pizza type?

Unsure if that's what you're asking, but I hope that helps!

Thread Thread
vetras profile image
vetras • Edited on

true. that is not what I meant. I mean renaming the original property named "size" to the new name "count" (as an example).

Refactoring is a big part of writing maintainable software and renaming is a basic corner stone of refactoring.
Hence my question.

Thread Thread
sam_piggott profile image
Sam Piggott Author

I think I understand - so you're suggesting if we renamed size to count, this would cause issues?

Type-checking would catch that (the beauty of TypeScript!). In the below example, we pick the two properties to create the new Pizza type...


type Pizza = Pick<Consumable, "size" | "caloriesPerServing">;

const pepperoniPizza: Pizza = {
    size: "large",
    caloriesPerServing: 500
}

Enter fullscreen mode Exit fullscreen mode

But if we renamed "size" to "count", as you suggested...


type Pizza = Pick<Consumable, "size" | "caloriesPerServing">;

const pepperoniPizza: Pizza = {
    count: "large",
    caloriesPerServing: 500
}

Enter fullscreen mode Exit fullscreen mode

Then we would receive a type error at compile time on the pepperoniPizza declaration line, as the object's shape (Pizza) does not have the property count.

We could then perform the changes we require on our type:

type Pizza = Pick<Consumable, "count" | "caloriesPerServing">;
Enter fullscreen mode Exit fullscreen mode

...which would resolve the issue.

Did that make sense? Hope that answers your question!

Thread Thread
vetras profile image
vetras

Did that make sense?

yes it does

Hope that answers your question!!

yes it does

thanks!

Thread Thread
sam_piggott profile image
Sam Piggott Author

Ah, that's great to hear. Sorry I misunderstood you initially! :)

Collapse
_rodrigomd profile image
Rodrigo πŸ‘¨β€πŸ’»πŸ€™

Hi vetras,

TypeScript doesn't actually throw run-time errors because it is compiled (transpiled) to JS, and that code is the one used in production, and because JS is typeless, you can override and change values ​​as you wish.

TypeScript will throw an error at the development stage because when the object's name is changed, it will not match the interface that was declared when the object was defined.

The IDE will highlight the error if configured correctly

Collapse
monfernape profile image
Usman Khalil

Fascinating. Makes development really easy. Before 'Partial', I used to make separate interfaces for almost similar objects