In this hands-on workshop, Maurice will personally guide you through a series of exercises designed to empower you with a deep understanding of React Server Components and the power of TypeScript. Discover how to optimize your applications, improve performance, and unlock new possibilities.
During the workshop, you will:
- Maximize code maintainability and scalability with advanced TypeScript practices
- Unleash the performance benefits of React Server Components, surpassing traditional approaches
- Turbocharge your TypeScript with the power of Mapped Types
- Make your TypeScript types more secure with Opaque Types
- Explore the power of Template Literal Types when using Mapped Types
Maurice will virtually be by your side, offering comprehensive guidance and answering your questions as you navigate each exercise. By the end of the workshop, you'll have mastered React Server Components, armed with a newfound arsenal of TypeScript knowledge to supercharge your React applications.
Don't miss this opportunity to elevate your React expertise to new heights. Join our workshop and unlock the potential of React Server Components with TypeScript. Your apps will thank you.
Practice TypeScript Techniques Building React Server Components App
AI Generated Video Summary
This Workshop covers a variety of TypeScript techniques, including type checking, compiling code, using operator satisfies, creating type mappings, manipulating types with template literal strings, and using opaque types for type safety. It also introduces Next.js and React Server Components for server-side rendering and direct data fetching. The importance of type checking and CI setup is emphasized, along with the use of satisfies and pick operators. Custom type mappings and their applications are explored, as well as the use of opaque types and type assertions for better type checking. The workshop also highlights the benefits of preventing undefined errors and using strict options in TypeScript.
1. Introduction to TypeScript Workshop
Welcome to this workshop. I'm going to show you a lot of TypeScript techniques that will help you be more productive and make better applications. We'll cover topics like compiling code to catch errors, using the relatively new operator satisfies, creating and using type mappings, manipulating types with template literal strings, using opaque types for type safety, exploring strict options in TypeScript, and looking at performance improvements for importing JSON files. The workshop will be interactive, so I encourage you to type things out and practice. You'll need Node, MPM, and Git to install and run the application. I've provided links to the GitHub repo and slides for reference. Let's get started!
Welcome to this workshop. My name is Maurits de Beyer, also known as The Problem Solver. Amongst other things, I'm a Microsoft MVP, Instructor, Troubleshooter, front-end developer, done a lot of stuff over the years. But that's not about me, so let's quickly skip that slide.
So what are we going to talk about? Well, I'm going to show you a lot of TypeScript techniques. Some of them you might be familiar with. Some of you might not be. But they're all sort of productivity things which will help you be more productive and make better applications. So in order to practice those, I've got a sample application which is a React Next.js application, using the new React server components because I really like them. You don't really need to know React for anything we're going to do in this course, although it is a little helpful. If you're unclear about anything there, just ask. For the most part, it's about the typescript, the typing and all of that. So some of the things I said we're going to compile code to catch errors, we're going to use it to relatively new operator satisfies. We're going to create and use type mappings, both use existing ones, but also mainly create new ones, which can be really powerful. We'll use template little strings to manipulate types. We'll use opaque types for type safety. We'll look at the strict options for typescript and discover that just setting strict to true is good but not quite as strict as it can be. You can set it to even stricter. We'll look at the performance improvements for if you're importing JSON files and more things like that.
So we're going to do a lot of exercises. All of this is supposed to be very interactive. And for the most part, I would recommend just type things out. I'll provide you with links to all the code I'm gonna write. You can copy it from there. It's all in the GitHub repo. You can look at the changes and copy it from there. And in some cases, I'm going to do that just in the interest of time. But for the most part, if that's all you do, you'll be much better at copy and pasting, which most developers typically are pretty good at. And you won't get a lot better at TypeScript. So actually doing it, making typos, fixing those, is gonna be a lot more useful, so highly recommended to do that.
Now, of course, we need some prerequisites in order to be able to install and run the application. Nothing strange. I would kind of expect everyone to be sat there. If you're using TypeScript, you're either a front-end or a Node.js developer. I don't have anywhere near the latest versions. This terminal kind of shows what I've currently got. Node version 16, if I'm not mistaking, the latest is 18, but there's no need to be on the bleeding edge. But you'll need Node. You'll need MPM, which comes by default, at least in almost every case with Node. And you'll need Git in order to clone the Git Repo. But even there, like you could get away with not having Git and just downloading a zip file. You'll also need a copy of the repo. And more importantly, the slides, because you'll see lots of slides like this where there is a little sample. All of those are links. And of course that opens in my other window. Here it is. So you'll see it opens links to the repo or to specific commits describing a specific change there. Let me move this one back for a moment. And let me... Nope. I didn't want to go to the next slide. I wanted to open that link. So, this one is the PowerPoint presentation. I would highly recommend keeping that open in the browser. And let me share that in the chat over here in Zoom. I also already added that to the chat window on Discord. So, if you've got that open, you can see a link to this as well. So that's the slides. Where was the other one? Where? Here. That's the repo. So I added that to the chat as well. I'll come back to those in a minute, but make sure to just open the slides and then you can find all the other links in there. So just for some context, we don't need to do that now, I already did that, but I created the next application using Create Next app. Pretty much took the defaults there. So, obviously TypeScript, because it's a TypeScript conference, but even if it wasn't, I would still use TypeScript because I'm a TypeScript fan boy. Use ESLint, although we're not actually gonna use that, but it's the default. I use Tailwind because that's the default and I also happen to like Tailwind. Use the source folder, not that important, but good practice. More importantly, though, I said yes to use the app router, this question here. If you want to use React server components, you have to use the app router.
2. Introduction to Next.js and UI Components
Next.js has two different routers: the old one with the pages folder and the new one with the app folder. The app router allows you to use React server components and new asynchronous components. ShedCM's UI components are used to make things look prettier. Each exercise in the repo is a separate commit, allowing you to see the changes made. Jean-Luc Picard is your cue to take action. The workshop is hands-on, with explanations followed by practical exercises.
Basically Next.js these days has two different routers. The old one with the pages folder and the new one with the app folder, which is called app router. That allows you to use React server components and the new asynchronous components there and a lot of cool capabilities, which we're gonna use. It's not the main focus, but it's still kind of nice.
And I left the default import alias there. Then just to make things look somewhat prettier, I used ShedCM's UI components. I'm not sure if you're familiar with those, but that's kind of the new kid on the block for UI components and really popular at the moment. I really like them. It's not like the traditional components where you install a big library and you get to use them, but you actually install component by component. And the source code, so you can just go and tweak them if you want to add styling, add options there, whatever you like. So I added those and a whole bunch of components.
In the repo you also find a lot of commits. Basically, every exercise we did is a separate commit. So if I go here for a minute, make this a bit, we'll do something about using read-only. If you click on that commit, you'll see exactly here what the change was I made. So before there was just a movie with a pic and it changed to read-only version of that. And something which would have caused compile error or would not have caused a compile error without that read-only. You'll also see Jean-Luc Picard come by. That's basically your queue to do something. I'll remind you, of course, in this case, you don't have to do anything yet. But all the other times it'll come up. It's kind of, okay, now it's your turn to do something. The basic setup is, I'm gonna explain something, I'm gonna do it, and then you get to do it. It's pretty much hands-on.
3. Setting Up the Application and Overview
To get started, clone the repository and install the dependencies. Prisma is already set up with a small database. Open Visual Studio Code to see the application structure. Run the dev script to start the Next.js dev server. The application is simple, with movies, a shopping cart, and a checkout form. Switch to the start branch before starting the workshop. Ensure everything is running smoothly. Any issues? RJ Perry, let me know if I can assist.
But the first thing we need to do is clone the repository. So if I go back to the repository for a moment, find the right window. I'll copy the link to the repository, go with the terminal window, and the standard stuff, git clone with that link and that clones. And by the way, you might've noticed from my voice, but I've got a bit of a cold, mild case of the flu. So my occasional sneezing, et cetera. Sorry about that, I'll try to mute the mic if I have to sneeze, but I might be a little too late.
So we've cloned the repository. Simple, it's not large, so that's not gonna take very long. The next step is do an NPM install to install the dependencies. You should see the usual NPM install stuff, but I've also set up Prisma for with a small database with some data here. So you should see some initial messages like you see on screen here about Prisma running a migration and then running a seed command and adding some data there. Let's go and do this. Change to the right folder, NPM install or NPM CI. Same effect. It does the installation as it should. And then when it's done, it should start configuring Prisma. Today would be nice. There, it's done. So we see it's installed in Prisma. So you should see that generated Prisma clients and running a seed commands. And it actually tells me there is an update but we'll ignore that.
Open up Visual Studio Code here. Just a quick overview of the application. There is an SRC folder, which contains a source. There is an app folder which contains the pages and the general layout. There is a components folder, with the components I wrote. There is a lib folder with some additional stuff. There is a server folder with some server side code we'll get to. In the components, there is a UI folder with the ShadCM components. They get installed there by default. And then a little higher up there is a Prisma folder with all the Prisma stuff but we're not really going to look at that. If I go to the NPM scripts, there should be a dev script there and I can run that and that should fire up the Next.js dev server and that should serve the application on localhost port 3000. Where did that go? Somewhere in here. Should see the application running on localhost port 3000. There it is.
So the application is really simple. You can click on Movies. There is a bunch of movies. You can select the next page. You can add one or more movies to the shopping cart. There is a little checkout form, specifies some name and some accounts. There is a bit of validation around here. Like the account number has to be four, characters long, one, two, three, four. I can press okay. And if I go back to Visual Studio code, down here you see a little message about the Checkout. You can select movies by genre. That should all work except don't go to horror because if you go to horror it will literally be that and you'll get a runtime error. But we'll come back to that runtime error and see what we can do about that later. For now we can see that it's running. There were no errors whatsoever in the console. So all is looking good. Before we start the actual workshop, you want to switch to the start branch. So let me do that. You can either go to main and you can just either use the commands there or just go to this little main here from the main branch and select origin slash 00 start. The application over here will still look exactly the same. But this is not with all the changes we're going to do. The main branch is actually a bit more complete. So make sure to do this before we actually start doing any of the exercises. And like I just had, you can either run it in the console or like I did from Visual Studio Code. And it should just appear to work as a normal Next.js application. No errors. Nothing. So the running application. And please go and do that.
OK, that's everyone back. So I noticed RJ Perry had some difficulties. You may just have to watch. Let me know if there was anything that can help. Everyone else was successful in setting up the application? Yeah. OK.
4. Type Checking and Compiling in Next.js
When setting up a Next application or similar, it's important to add scripts for compiling and type-checking code. Development environments often prioritize speed, which means the TypeScript compiler may not be called to type-check the code. This can lead to unnoticed errors. SWC, used in this case, removes types and treats TypeScript as JavaScript, which can hide errors. To address this, a script can be added to manually type-check the code. Another option is to use the TypeScript watch feature, which automatically reruns type checks on code changes. It's important to type check all code, not just the code that is shipped. TypeScript errors can be readable but may require additional tools for improved readability. In this specific case, there is an error with an argument type mismatch due to a pick operation extracting only a subset of properties from the movie type.
Good. So pretty much any time you set up a Next application like that or pretty much anything similar, the first thing I do is I add some scripts to start compiling and type-checking code. The reason is that most development environments are set up for speed. And that makes sense. You want fast feedback. You write some code. You want to save it. And you want to see the result immediately. And as a result, things like ESBuild or SWC are getting a lot more popular with TypeScript development.
And in general, it's like we do as little as possible to make it as fast as possible, which makes total sense. But that means that, at least in most development environments, the TypeScript compiler itself is not actually called on the codebase to type-check the code. And there could be errors in there, and you would never know. And in fact, there are errors in the code here, and I actually executed some of the code just now where there were errors, but we didn't get to see any of those in the console or anywhere. It's like they didn't exist.
Well, in this case, it's an XJS application. So that uses SWC, which is written in Rust. And that kind of pretends to compile the TypeScript code, but in reality, what it does, it doesn't do any type checking. It just removes all the types and treats the resulting TypeScript as modern JavaScript, which is pretty much true. Like if you remove all type annotations from TypeScript, it is for 99.9% just more than JavaScript. And that's fine, that's good in most cases except when there are errors. And in some cases, you'll notice those errors.
Like if I go to Visual Studio Code and I stopped this running for a minute and I'll still start the next build to build the code. I should be able to see one of those errors, but it doesn't show up until I actually do a build. And that's something I don't do in the development cycle because it takes too much time. So now it's doing the linting and checking validity and there it actually comes up with the error. But it's quite possible that you'll have type errors in your code which don't actually show up when you do a build. For instance, you might have type errors in your unit tests and those are not included in builds because their unit tests just used as development time. They never actually ship with the clients or server bundle. So they're never actually touched and checked as part of the build step.
So we want something which gives us feedback a bit faster than only when we build. And we also want to type check all of the code, not just the code we ship. So one thing you can do is this. You can add a little script. I call it compile or type check. In this case, I called it compile, but it's not really compiling. It's more type checking, and I'm just calling it TSC, so the TypeScript compiler, which you have, cause TypeScript is installed. I've actually got no emit option here just for clarity, but in fact, you can leave that out because that's already in the TS config file in almost every case. So if I add that, let me go to the package station. Nope, not next build. TSC. No commit. Now I can... Where's that script? There it is. I can run the script. And that will tell me when there is a compile error. A bit faster than doing that next build. Still, it's something you manually have to do. In a large program, it takes a while. So that's what I actually do. But in a small program with a relatively small code base like this that's plenty fast. So what I actually add is a second one where I use an additional TypeScript feature watch. So we'll add that option. That shows up there. So we'll run this one. It does exactly the same type check action but now it stays active and anytime I make a change to the code it will automatically rerun. Now with a small program like this this is what I typically do in development mode with larger programs where you've got a large team working on it. Just have a type check like that. Becomes a bit slower so I don't do this I move it to a Pre Commit hook or maybe a Pre Push hook or something like that. I'll run it on the server as well. But let's actually go and check what the error is here. So apparently there is an error here it takes a while to show up but there is some kind of argument of type and there is a pick with a whole bunch of stuff in there does not match something else. You've got to love those TypeScript errors they're really readable. Long list of stuff. This actually makes it a bit better. I've got a plug-in Visual Studio Code extension which makes the error more readable but even there it's still kind of like is not a designable that object is not a designable to something of this shape. So what's actually the case here? Well, if I, let me just go back so that movie is prop which is being sent into these components, that's defined up here and that uses the big type mapping from TypeScripts to take a movie type and then extracts only a bunch of properties. So not all of them. So this movie is only a subset of the complete movie shape. That's being sent into an AddMovie function. And that AddMovie function is defined right here and that expects movie which is a complete movie, all of the properties.
5. Type Checking and CI Setup
In this section, we encounter an error related to picking a subset of a movie. The error message indicates that using a subset of a movie does not match the complete movie type, which makes sense because the shopping cart only needs minimal information about a movie.To resolve this, we create a type mapping specifically for the checkout process, defining the required properties as 'ID' and 'title.' This ensures that the movie used in the checkout process adheres to this specific type.In summary, we've discussed how to address a type error, the importance of local type checking, and the need to integrate checks into the CI/CD pipeline, like GitHub Actions. This helps catch errors early and ensures code compatibility with external components.
Now that pic just took a subset. So basically the error is telling us, well, a subset of a movie does not reflect the whole movie. In fact, in the shopping cart, we don't need to know the whole movie. You don't need to know what's the number of votes and the rating and the whole description and the posters of a movie is. You just need to know which movie it is, so the idea, maybe the title in order to print it. So let's create a little type mapping here as well to fix that type movie for checkout. Is the pic which I had in the other of movie. And in this case, I just want ID prop and I want the title. What's wrong here? And that shouldn't be a comma, but that should be an r. So now if I say well the movie we need in here is actually of that specific type. Replace all the types of the movie. I save and no errors. The TypeScript compiler is completely happy. Now before I actually did the whole checkout flow, so we did the checkout flow with this type failure. And even though we had that there were no errors whatsoever. So having this type check action and watching those errors, which is why I typically open those in VS Code itself is pretty helpful. So the scripts I added, the error we had when we executed the type checking, the fix I did. And the result is no error. So please go and do this. So typically, as I mentioned, this is one of the first things I'll do when I start a new product, set up the TypeScript compiler, type check the code locally. But of course, you don't want to be dependent on every developer on the team doing all the checks locally, running all the unit tests, maybe running Prettier and having all of that taken care of locally. So you typically want to make that part of your CI. So in my case, that typically means GitHub. It's pretty popular, but of course it's not the only option out there. So people might be using something else. But I'm just going to assume GitHub for now. I want to make sure that all type checks etc. are done. So I want to run that TypeScript compiler which we just did locally. Whenever I create a pull request and have the CI server check on that. But I also want to do other stuff. Quite often I'll work with GraphQL and there will be another team doing the GraphQL work so kind of two separate repositories. And I quite frequently run into the case where they've made changes to the GraphQL server. And our client code compiles just fine. It just happens to compile against an out of date type definition from the GraphQL server. And the same could happen with databases with open, with REST services if you use something like Swagger or OpenAI. And do type generation that way. So I typically want to regenerate those as part of my CI and make sure that I compile everything against the latest version catch errors as quick as possible. So assuming GitHub for a moment, I might have an action, which looks something like this. So on every push to main or every pull request against the main branch, I'll run a bunch of steps, check out the source code, set up my NPM dependencies, then run the TypeScript, compiler, run lint, run tests. I didn't actually set up any unit tests here, so that won't actually work. Run the build, so make sure we've got all the artifacts. Save those artifacts probably so we can just reuse those, not rebuild all the time. Not actually gonna do this right now, but it is in the repo. So if you click the image, you'll get to that actual script, which you can use as a basis. There's a lot more you can do in here. I typically have playwright for end-to-end tests. I'll include that here as well and then I might set it up with multiple parallel jobs in order to speed things up a bit. It all depends on how big and how complex the project is.
6. Introduction to React Server Components
React Server Components allow for server-side rendering of components and direct data fetching from a database. Traditionally, React assumed components would re-render on the client, requiring additional steps to access data. This complexity can slow down the application.
The next thing I briefly want to talk about is React Server Components. What are they? Why are they new? And why am I so excited about React Server Components? Because traditionally React has been purely client side environment. And that's not completely true because there was also server-side rendering with React, where you could render components server-side and then ship them to the clients. But React always assumed that all of those components would re-render on the client, be re-executed and basically do everything there. So server-side rendering was an option, but it was just an optimization. So you didn't start out with a blank page and get something there. But that also means that if you want to get some data from, say, a database, well, your components have to be able to run in the client, in the browser, and you can't access a database directly from a browser. So you typically had to add REST services there, or GraphQL, or something along those lines, to communicate with those kinds of resources. And that meant additional steps you had to go through, and those make life a bit more complex and make the application run a bit slower.
7. React Server Components
In a typical React application, components render without data and use an effect hook to fetch the required data. This gradual process can be improved for a faster feedback cycle. React server components allow rendering on the server, fetching data directly from a database, and waiting for it to be loaded. The client receives the rendered result without re-executing it. Interactive components still run on the client, while data fetching and rendering happen on the server.
In a typical React application, if you needed some data, say you needed the list of movies like we have, your component would render, not having any data. It would then use an effect hook to trigger a side effect, which is in this case fetching of that movie data. And then when that was completed, it would update local states, causing the component to re-render again with the actual movie data. Of course, the links to the images would only be available then. So then the browser would kick in and say, oh, well, you want to show some images for these? Let me actually go and fetch them. So it's very, well, gradual. There are lots of little steps before you actually have everything on the page. Unfortunately, browsers are pretty fast, and networks these days tend to be relatively fast. So it isn't bad, but it's nice to have things a bit more integrated. One thing that always drives me is a fast feedback cycle. I want to have feedback when I'm developing as fast as possible. I want to have feedback for end users as fast as possible. For end users, because it makes their experience nicer, if you first open a web application, it renders a blank page. Then it renders some, well, very little UI, and only then does it start rendering the actual UI. That's not such a nice experience. For a developer, if you have to go through lots of different hoops, before you actually see something which might be wrong, that's not so nice. I want to have fast feedback, short cycles there. Well, before React client or React server components, React components would kind of look like this. So all of them would be rendered on the client, in the browser, and then on the server. And I'm kind of ignoring server-side rendering here, but with server-side rendering, like if we had something like this, well, the application would render on the server. The MovieList components might pre-render on the server, but it wouldn't fetch any data. So basically the movie carts and et cetera, wouldn't render. All of that would basically be delegated to the client. Well with React server components, we can do that a lot more directly. We can have the application render on the server. It fetches data directly say from a database and the component actually waits for those items to be loaded from the database. It can handle a single scope directly. It renders all those movie carts and all of that happens on the server. The result gets shipped to the client, but it doesn't actually re-execute on the client. So the client is much simpler, just shows that. Now that works for those because they're not interactive. It's basically a static list. Yes you can navigate between pages, but all that navigation stuff that's interactive. Well those actually have to run on the client. So there might be a rate movie component where you can do a thumbs up on a movie, thumbs down, or there is a movie editor where you can change details on the movie. Those would be client components rendered on the client, but all the other stuff, data fetching, et cetera, renders on the server.
8. Leveraging React Server Components with Prisma
React server components allow for server-side rendering of components and direct data fetching from a database. Traditional React assumed components would re-render on the client, requiring additional steps to access data. This complexity can slow down the application. Server components in Next.js enable rendering on the server, fetching data directly from a database, and waiting for it to be loaded. The client receives the rendered result without re-executing it. Interactive components still run on the client, while data fetching and rendering happen on the server. All server-side code is only run on the server, not in the client, making it secure and reducing the size of client bundles. To use React server components in an existing application, TypeScript 5.1 or later is required.
So we can see that, for instance, if I go to the movies page, movies... There, the movies page. This is a React server component. And at first glance, there isn't that much to tell, like it's just a React component. It says a bunch of markup in here, JSX. The only slight difference here is this is actually async, which is new with React server components. We can have async components. And in here, it actually calls this getMovies components and, or getMovieData, I should say, and awaits that. And if we look at that getMovies. It's an async function, but what does it do down here, it actually uses Prisma and goes directly into the database to fetch data. So no Ajax requests here, etc, everything is a lot director. If I go into my Prisma definition and I change some fields, like I might change overview to description, then I'll get immediate compile errors here, saying, well, the movie we're working with, which is the movie Service from Prisma, doesn't work anymore. Now, if you've worked with Next.js before, you might think, well, didn't we already have that? We had getServerSiteProps. And in a page like this, it wouldn't be async, but you could call getServerSiteProps and you could execute database logic in there. And that's true, you could, but that was only for pages. If I go back to the application for a moment, where is it? There. Here, we've got a list of movie genres and I can select the genre, and say, just give me the items for that. And apparently I've stopped running the application. Let's start it again. Is it running? Today would be nice, yes. So now I can actually switch between different genres. So this list of genres is obviously not hardcoded. I need to fetch it. But that's a component somewhere in a navigation bar, not a page. Well in Next.js with getServerSiteProps you could only work at the page level. Right now I can do that on the client level. So if I go to the navigation, where is it? That's the actual component. So this is a component which is rendered inside of the navigation bar. It can still be async. It can still go to Prisma Databases. I just find me all the genres, order them, sort them, filter them, whatever you want to do. And then it goes off to another component which has the interactive stuff because this reacts to clicks, starts filtering, etc. So I can do that with any component I want, which is really nice. And the GetServerSitePropsFromNext was next specific. This is standard React. So at the moment, Next.js is pretty much the only framework which supports this. In the future, this is going to be supported by all the others as well. And it's learn once, run everywhere. So React Server Side components, really nice. One really neat thing is all the server side code is actually only run on the server, not in the client. So all of that code is not bundled into the client bundle which means if you have to use secrets in order to access a database, API, keys, that kind of stuff, it's completely safe. It's never shipped to the client. It also means that the client bundles are smaller because there's less code shipping there. Now, if you want to do this with an existing application, you kind of have to keep in mind that it requires TypeScript 5.1. Before TypeScript 5.1, TypeScript was not capable of working with asynchronous components. They kind of had their own definition of what a React component looked like. Now they actually updated that to follow the official type definitions and they're fully capable of working with asynchronous components.
9. Server and Client Components in React
Server components are considered server-side rendering components, while client components are interactive components in React. The use client string is used to specify a client component. TypeScript introduces the satisfies operator, which has various use cases. Let's now focus on some pure TypeScript concepts.
So if we've got server components, we probably also have something called client components. And client components are the ones in React which are interactive, and you actually specify that something is a client component by putting this little string at the top, use client. Might look familiar in the past in JavaScript. We had this you strict, which put JavaScript in a strict remote, kind of to default now, but they kind of leaned on that, borrowed that idea and just put that at the top. So anything which is not a client component like this is considered a server component. So here's server component, like I just showed. Now we're going to use that, we'll see a bit more of that later. But first let's get back to some pure TypeScript stuff.
10. Understanding the Satisfies Operator in TypeScript
The satisfies operator in TypeScript helps identify potential errors in code. By using it, we can catch errors related to missing properties at compile time. This operator is particularly useful when working with complex data structures and selecting specific properties. It ensures that the selected properties are accurate and prevents runtime errors. Additionally, the satisfies operator provides a more reliable way to define the type of the selected properties, making the code more robust and maintainable.
That's a relatively new TypeScript operator, so the satisfies operator. You might have seen it. It's not exactly from last month, but it's one of those operators where most people think, well, kind of neat, but where do I use it? And at first I couldn't really find many use cases for it either, but it turns out there are some interesting use cases. Because we've got an application with compiles, but there are actually some potential errors there. And let me go and introduce an error. Like, it's running. We can render movies, just go here. No problem, no errors. No errors here in the console window either. All of that works just fine. Those movie cards are rendered by this movie component and there's this vote average. Like, that's this one. But what's a vote average if you don't know how many votes. Like, this is a vote average of 8.7. But if there are only two people who ever voted for this and there's a million who voted here then that kind of skews those numbers a bit. Turns out the movie actually has that. So here we can say I also want the vote count. How many votes were made? So I added that here. No compile errors so let's actually use it. So we'll include the vote count here. Okay, so many votes. There are no compile errors, nothing. No runtime errors in the UI even if I check the console. There is some little message there about infra but nothing else. But still if we look here it says well bracket open blank vote. So there's no actual data there. And I might say, well, I actually wanna make this somewhat nice format. So we'll do to locale string and we'll say, because I'm in the Netherlands, I want this in Dutch locale. And let's do the same for that vote average. And I guess I need one more bracket there. Still no compile errors, except now it does come with a compile error and I wasn't expecting that. Why is that? Can't read property. Interesting. Oh, I'm not actually looking at the compile script. I was looking at the runtime, which I was expecting there. Yeah, no compile errors. But if I go to the application, just like we just saw now with the But if I go to the application, just like we just saw now we have a runtime error because vote count is actually undefined. Well, I want that error before I actually run the application. I want to know from type script that there is something wrong. But no compile errors, how come and how can we fix that? In that movies page, over here. I have imported that movie card and I've set the movie requirements for the cards are basically the definition of that movie prop. And that vote count is there. It's a number. Yet in my get movies, if I check the results, it's kind of claims that it's returning everything. So I want to make it visible. So it kind of says, well, vote count is there, but there is a relate date. There is a poster path. There is a backdrop path, overview title, et cetera. Popularity, there are genres there. But if I look at what I'm selecting, I'm only selecting these fields from Prisma. So the ID title, overview, backdrop, and vote average, the vote count is not in here. So how can I make this a bit more reliable? Well, that's where the satisfies operator comes in. So if, instead of saying the select is of type MovieSelect, which is basically the type Prisma generated, which I can use for the select, I can say no, it's satisfies MovieSelect. And now all of a sudden I get a compile error. And it indeed, it complains about voteCount being not there. So what's the difference between that we undo this change, between having this, where I say, select is of this type, or using the satisfied? Well, in this case, if I look at the result of select, and I kind of have to look in here, it's a little hard to read, but there is a mapping where it takes all the properties from the movie object and sets them to be of Boolean type. So you can select them, yes or no, and makes all of them optional. But the result is that this select variable, according to TypeScript, knows all of those keys. So what does TypeScript think? Because we use that Select here on the Find Many, it thinks that this result is all of the keys which are listed in there, whether we actually select them or not, all because the Select is of a specific type. Now, if I redo my change with the Satisfy operator, now the type of Select is just the properties listed in there. And now TypeScript is aware that's where I return movies. It only contains these properties and the function is set to return all of them, including the voteCount, which it doesn't. So now I can add voteCount and we're good to go. No errors. Now let me comment that out, because one thing you might also wonder is, can't we just do this? This also gives me a compile error. The downside, though, is suppose I make a typo in here. And I add that voteCount back in. Now it actually complains about voteEverage, so let me make this another error. Like this. I'm selecting some completely unknown data.
11. Using Satisfies and Pick in TypeScript
The satisfies operator in TypeScript allows us to check that a type is valid and contains all the necessary properties. By using satisfies, we can catch errors related to missing properties at compile time. However, there are limitations to satisfies, such as not being able to select specific properties from a type without copying them. To address this, we can create a new type and use the pick operator to select only the properties we need. Additionally, we can make all the selected properties required by using another type mapping. This ensures that we get compile-time errors for any missing or invalid properties. Overall, using satisfies and pick can help us optimize our code and prevent unnecessary data fetching.
So property doesn't exist, but no error is there. Property doesn't exist but no error whatsoever. So with the satisfies. Do I still? No. What was it, the movie? Don't want that. Where was it? Prisma.movieSelect, I think. So now it actually complains here, well, this property doesn't exist. So satisfied in cases like this kind of gives us the best of two worlds. It doesn't change the type, but it does check that the type is valid and you've got everything you need in there. Let me save so it compiles again. No compile time errors and no run time errors and the VokedEfforts with the actual number of votes is printed there nicely. So the change I made using the Satisfied operator here. So please go and do this, and after that we'll see how we can make this select even better, which is kind of nice. We've kind of fixed the problem where we're not querying enough data from the client and if we do, then we get an immediate compile error, not some run time error. But fixing the other way round where we're fetching more data than we need would be kind of nice as well. Because right now I could go in and say we'll select all lot of things and we need that backdoor path, but there is also a poster path, so let's select that as well. It needs a comma there. It runs, compiles fine, no problem, if I go to the browser, it renders fine, but we're fetching more data than we need. Now, is this terrible? No, but it does make our application slightly slower, and if we can prevent it easily, that's kind of nice. Well, that's where Satisfies breaks down. Satisfies basically says, okay, we're selecting data which is available by having that satisfied Prisma Movie Select, but we've got the keys we actually need. And instead of using the Satisfied for that whole Prisma Movie Select, we can make it a little bit more restrictive and say, well, we only want to select the data we actually need. So doing that isn't all that hard. We can basically create a new type, do a little type mapping. So we could say, MovieToSelect for instance. Is, and then we could start with saying, where's that? Is that MovieSelect? Let's at least spell this correctly. MovieToSelect, we'll put the satisfied in here. And now we basically have exactly the same, because I've created the type alias saying that I've got a type MovieToSelect, which just points to MovieToSelect, no big deal. But now I can say, well, I don't want everything from MovieToSelect, I want a subset of that. And just like I did in the MovieCards where I said, where was it? Over here, we've got a MovieType, but with pick I can say, well, we only need these properties. I can kind of do the same thing. So I could copy this and say, we're gonna do a pick here. Pass in those properties. If I can spell pick correctly like that. It actually tells me, okay, poster path shouldn't be in there. But this is kind of like, meh. Because I had to copy all of those keys from one component to another. And now if I go into the movie cards and I change the number of properties we actually need from the movie, I kind of have to update this list as well. So it works but, hardly ideal to copy those. Instead, we've got this type here. The movie required from the cards, which is actually, I refer to the movie cards. I get the component props of the movie card and then using this array syntax with the property name movie, I get the actual type defined in there. So this component props basically says, okay, go and look at return this props type. And then with square bracket notation you can go into that and get any child type from that. So look at the movie prop, which just happens to be the only one and get the type from that. So I kind of would like to reuse that, but I can't just put this in here because that's not part of how Pick works. Pick, if you look at the syntax, It takes a type and then the K or the keys you want to pick from that type. But with a simple keyof, we can actually turn this type into just a collection of the keys. And now if we look at movie to select, it's just the difference properties we want. So again, poster path is invalid. Now that sort of works, but I could set this to false for instance, or I could remove something else. And that would mean that those don't compile, let me call this one out because that's definitely invalid. Now it's kind of like there is an error right here in what I'm selecting, but that error only shows up here in the result type. And I kind of want that somewhat more immediately. I don't want to allow false and the vote count here is not missing here because if I look at the movies to select, all of these are defined here as optionals with a question mark or undefined. So I can use another type mapping here, well, I don't want just the optional parts. I want to make all of these required. So wrap all of this in another standard type mapping, and now movie to selects are no longer optionals. It's like they're all ID booleans, et cetera. So right now, the compile error is here, saying, okay, this doesn't actually satisfy. So add the vote counts back in, that actually satisfies it. And if I put PosterPath back in, that would fail. Still not quite that nice though. There are basically two small problems I have. First of all, I can save the vote average should not be selected by specifying false, because this MovieSelectType, that's a little unreadable. Let's take this one. It doesn't say whether that it should be true, it's boolean, so I can select whether I want to select a field or not. So by not selecting vote average, I actually get a compile error, but again, somewhere down later down and then I kind of have to shift through all of this and figure out why there is a compile error there. So this works, but it's kind of like, hmm, could we do better? Well, let me comment this type out. Oh, by the way, the second problem I have with this, it's kind of like, if I want to understand what MovieToSelect is for type, it's kind of like I have to go and inspect this type, which is kind of hard to read.
12. Custom Type Mapping in TypeScript
To create a custom type mapping in TypeScript, you can use the 'type' keyword followed by the desired name and the 'is' keyword to specify the type. This allows for more readable code compared to using 'get require', 'pick', and 'keyof'. Custom type mappings can be used to select specific properties from a type and ensure type checking. The type system in TypeScript offers a lot of capabilities, including loops and conditionals, making it a powerful tool. One example is using Zot to create object or type schemas for validation. By using 'z.infer', you can infer the TypeScript type of the schema. While this is an advanced technique, there are simpler cases where custom type mappings can be beneficial. To experiment with custom type mappings, create a new file and play around with different types. The most basic typemapping uses the 'type' keyword followed by a name and the 'is' keyword to specify the type.
I have to look at this, well, that's relatively similar. I have to keep in mind what MovieToSelect what pick does, so there is a key of here. So pick takes all of these. Then only takes the ones which are listed in here. And then require modifies that type again with that minus question mark, which removes the optionality. So there is quite a lot of stuff going on here. And if I wanna understand what's going on, there's quite a lot of detail to look at and IntelliSense kind of makes this hard. But what I really want is I want kind of this movieRequiredCard. But instead of having the types Number, String, I just want through there. These are the ones I need to select. I don't even want Boolean in there. I just want true in there. So I could just copy this And say, Movie to select is this. And then select all of these. And say they should be true. And comment out this because we've got the same type definition twice. I'm not sure what's the definition. I'm not sure why I've got type type. Think I've did something weird when I was copying. And now I've got the error here saying, okay, this is false. Set it to true. And we're all good. Uh, I had to post our path and that would compile. Give a compile error there. But now I'm back to okay. I've listed those same properties there. So how can we prevent that? Well, using that same key of. What I can do in here is I could say, well, I want, or a prop in key of that. And I want to define that as true. So now movie to select still looks exactly the same, but it's based on the keys of what I require. So remove this and I've got type checking. I go back into the movie card say, I want something else say I really want that movie path. Posture path I meant. Now I immediately get the compile error here saying that poster path is missing. Add that and we're good to go. I'm not actually using it. Remove it. And then I get a compile error that I'm selecting too much data. So with a pretty simple type mapping, personally, I think this is more readable than using get require, pick, keyoff, etc. This just makes it much clearer to see what's going on and get exactly the same result. We'll be doing a lot more of custom type mappings, like this. The first way of doing it with a combination of some of the standard type mappings. The alternative, which I think is slightly better. But whatever you prefer. The end result is exactly the same way. So please go and do that. And then while I open up the breakout rooms, like if I can help the people who were having problems getting the application to run in the meantime. And let's see if we can sort that out and all the others can continue with this exercise. So see you in a minute. We're doing a bit of custom type mapping here. We're doing a bit of custom type mapping before, and turns out custom type mapping is quite an interesting process. The type system in TypeScript itself is kind of a programming language. And you can do quite a lot of interesting stuff there with types. So I just want to go over some stuff and show you some of the capabilities there. You can use loops, conditionals, and that leads to some pretty, well, advanced stuff. And one of the nice examples is, for instance, if you use Zot, maybe you're familiar with Zot, maybe not, but it's a way where you can create a schema for objects or types and then validate them. So you might have something like this, a Zot schema with the first name, last name H. And, say, with the first name, you might have things there like it should be minimum of two characters long and the maximum of, say, 50 characters. And the H is a number and it's optional like this, but it's also not less than zero because you can't be minus 25 years old and it's less than 125 because no one is 200 years old. Stuff like that. But quite often you need the types, and then with something like this with z.infer, so Z is the Zot object, you can infer the actual TypeScript type of that schema. So in that case, the user here would consist of a first name of type string, last name of type string, and an age of an optional type number. Now that's a pretty advanced thing if you look at how that's done, but there are lots of simpler cases where you can start using them. And I just want to go and play around with that a bit so we can see some of that stuff. So, let's let's create a new file here in the lib folder, typemapping.ts and we can play around with some types. The most simple typemapping is just used to type keyword and say give it some name, you type is and intellisense comes up with a bracket, but in fact we can just say is some other type, movie for instance or I could say is string. So that's a really simple typemapping, and when I hover over new type it says well, it is really just a string, or if I say movie it's really just another alias to the movie type, so it doesn't really bias a lot. But as we saw before, we can open up brackets and we can just specify a type in there so I could do X is a number, or an old number. And we have a new type like that. Kind of a fixed type.
13. Type Mapping and Conditional Logic in TypeScript
We can loop over the properties in the movie and define a new type. TypeScript treats two shapes as the same type if they have the same shape, even if the type declarations are different. We can make the type more general by adding a parameter. We can also add conditional logic to the type mapping, specifying different types based on property types. We can use 'extends' to determine if a property type matches a condition. We can filter out keys based on their associated type using the 'extends' condition on the key itself. Using 'never' allows us to remove unwanted properties from the type.
But as we saw before we can also start looping over, say the properties in the movie so I could do with square brackets, define the prop name and then say for the prop in keys of movie and then pass in some type, for instance like this. So now I've taken all the keys of the movie and I've defined the type as Boolean. And I've got a new type. So all the properties that are known from the movie type are there. Just have a different type. I can also use the original type if I want. So we started with movie. And using the square bracket notation I can index into that type. So if I specify the prop, it actually takes that same prop. So now a new type is a copy of movie, but it's really a copy and not just an original reference. But effectively, it's the same because TypeScript has a type system where it says, if two shapes are the same, then it's considered the same type, even though the type declaration might not be. Which is really nice, like for instance, a language like C Sharp doesn't have that. You might have a movie object defined with a movie type. And you might have another object defined with a type which is exactly the same shape, with a different definition. So they're semantically exactly the same. But Typescript will say, yeah, well, you can't use one movie in place of the other because they're officially a different type. Typescript doesn't, it says, well, they're the same shape, so they're really equivalents of each other. So whether it's a different shape or not, it doesn't matter. But here we can kind of see, well, we can loop into that, loop over the props and the keys in a movie and index into the movie type. But in this case, it's still hardcoded to a movie. So suppose we want to make this a bit more general purpose, it's not a very useful type, but assuming it would be, and we want to do that, we can kind of add, well, let's call it the parameter, and traditionally, you'll see something like T there. And we can say, grab the properties of T and now new type itself isn't anything useful, it's a type mapping which still needs a parameter there. So I could do, say, type new movie. is a new type and there are specified in movie. So this new movie, how many typos can you make in one word, but is still a copy but now it's parameterized. So I could also do something like, define an inline type here, say it has an X of a number and a Y of a number, we'll call that location, and we can see it location XY. Now this is still pretty simple inline type, another reference type and it just makes a copy so there isn't all that much point in this yet. So let's actually fix my typo there, new movie. But now we could start saying, well, if I can loop over it, I can start adding conditional things as well. So if you look at that new movie, it has properties of type numbers, string, and I think that's it, yeah, just number and string. So I might say, well, if it's a string I want the original type, but if it's a number I want the Boolean instead, so we can start doing logic. Now, how do you do logic in type mappings? You basically look at the type and you determine if something matches by using extends. So I could say this extends string and then it's just like before syntax, we say, for instance, I'll just hard-code two strings, Now string or not a string. And now if I look at new movie, I've basically created a new type where the ID is, well, it was number, so it's the string not a string, and title is, it's no longer of type string, but just a string. So in this case I might say, well, this should actually be a string and this should be maybe a boolean. So I've put logic in here saying, well, if the property is of type string, then it's going to be a string or it's going to be a boolean. And in this case, we'll see ID, which was a numbers become a boolean, title is still a string, et cetera. And of course, instead of saying it should be a string, I can turn this into this prop as well. So just keep the original, which works out to exactly the same thing. I could also, let's copy this so we keep it, say I want only properties of type string, so, and I can say, we'll get movie-strings is properties of type string, movie. Well, right now, of course it doesn't do that because I just made a copy of the original. So now for anything which wasn't a movie, we still get Boolean. Now with TypeScript type mapping, if you want to get rid of something, you can use Never. So the first thing people typically try is, well, I don't want this Boolean there. I want that not to exist. So I'll put a Never there, which doesn't quite work. Because now if I look at movie string, it says ID is Never, but suppose I actually use this type, const m is... And then I start filling it in and then I have the backdrop path for instance, and all the others. But if I look at the error, it's actually gonna complain, well, it needs an ID. But that ID is supposed to be type never. So what are you gonna put in there if it's a Never? That's not valid. So we kind of want logic like this with that propExtentString and a Never, but we don't want it here with the type. We actually want it with the key. So we can do the same using the s. We can say, well, if this extends......a string, I should have copied that as well. Then we actually want that prop. And if it isn't a string, we want Never. And... Now I've got a typo there and I'm failing to see what my... Question mark? Oh, right. Yeah, so now if I look at movie string only the keys which were of type string are still left. And we don't actually need all of this anymore because that part of the conditional is no longer valid. We're just doing it on the key part itself. So we're filtering out the keys where the type associated with the key is a string. And otherwise we'll replace it with never which makes it go away. And of course I could do the same with saying we only want a number. Maybe the naming of the type is a bit strange now movie strings which are number, but that works. Or we could say this is a boolean. And that's going to leave us with an empty object. Nothing, because there are no boolean properties there.
14. Making the Type More Flexible
We can make the type more flexible by adding a second target and specifying the desired type. This allows us to select specific properties based on the specified type, making the code more flexible and adaptable.
Or we could make it more complex. We want boolean or string. In which case we're back to all the strings because there were no booleans but otherwise they would be combined. So if I do a number or a string we'll get both of them again. Pretty much get a copy of the original again. So that's pretty neat. But it's still hard coded to a specific type here. So could we make that another variable? Could we do something like I want this but I want of some specified type? So, well, we can go in here and add a second target. So we add the target there so I could do type P1 I just need some name. Is props of type movie specified as number? So it only selects the number or I could say, give me the string ones. And only get the string or I could say string or number, or string or number or Boolean. So I kind of make it very flexible.
15. Naming Conventions and Descriptive Type Mappings
Type mapping is like a function that you call at compile time to work with types instead of objects or variables. Naming conventions for type mappings should be descriptive and meaningful. Use variable and parameter names that make the purpose of the type mapping clear.
There was one thing I did here, which I typically don't like, but you see that a lot with type definitions. Typically you see these generic types. So defined as T or K, one letter. And you should really think of type mapping like this, is it's a function. Just another function you call it runtime, but a function you call at compile time to work with on types instead of objects or other variables. And are you going to name your functions like get me something with parameter T or I or something like that or M for movie? No, you're probably going to name it movie and maybe in a loop you'll use I, but typically more likely you'll use index. So I typically say well this shouldn't be T, it should be more descriptive so I'll refactor that to the object. So the type of the object and you might wonder well do we actually need that T that's kind of like a Hungarian naming convention. We're only talking about types here so just saying object would be fine except that kind of conflict with the regular object type so in that case I would leave it T. But I might like here I'd have targets and maybe source. Something like that to make it clearer. You'll see some other examples later where I've actually taken type mappings from standard or shared utilities from others where they use T and K and U and things like that and every time I kind of have to read the type mapping and then what was the order what does what mean again. So, use meaningful names, it's really just a variable name a parameter name and you can do whatever you want with that.
16. Type Mappings and Utility Functions
You can use default parameters and assign a default value to make the code more flexible. The syntax for type mappings is limited compared to the full TypeScript language. Debugging can be challenging, but with experimentation and tweaking, you can create useful utilities that enhance TypeScript's capabilities.
Another neat thing is like you might say well I typically want to use this with a string so I want to be able to use it with this, I have a default parameter there so you can do that as well. Just assign a default value here. So if I pass something in, let's change this one to a number. It works with this, so p1 picks up all the numbers. If I don't pass anything, it will default to this string. So p2 is actually all the movie properties of type string. Useful to make things work. So it's a bit getting used to the syntax because it is limited. It's not nearly as complete as the whole TypeScript language normally where you can write if statements or use in line conditionals just like you can here, you have to do it this way. It's also harder to debug if they become more complex because it's not like you can just debug through this, put a break point here, say I want a break point here and then step through it. At least I'm not aware of any way of doing that. So it's kind of like you change stuff, you hover over it, you see what the result is, like what do I get out of this? And then you tweak them that way. So it is a bit harder and it is a bit limited. But in the end, you can make some really nice utilities with this which will help your TypeScript a lot, make things a lot typesaver and more descriptive and things like that.
17. Type Mappings and Testing
So I've got some examples here, basically what I just did from copy an object, copy objects of a specific type. You can actually put conditionals in here as well. The keys don't always match. Objects in JavaScript, the keys don't have to be strings, they can be strings, they can be numbers, or they can be symbols. Another useful thing, if you want to create type mappings like this, you also want to test them, just like with any other code. Useful to type check, your actual type mappings. So please go do that.
So I've got some examples here, basically what I just did from copy an object, copy objects of a specific type. and basically combine all those things. Oh, this is actually one I forgot to mention, like what I could do now is, if I say props of type, like I could do this. I say I want that of a string for instance. So props of type, well, this really doesn't make a lot of sense to put string in there. This is kind of designed to go over an object and map over the keys and I'm not even sure what. It just comes back a string, even though I said, well, I want the property types which are of type number. So you can actually put conditionals in here as well. So again with the extends, just like we did with Boolean logic we can say extents something. We can say, well, in this case, the source should extend an object. So I could say extends object. Extends object. It turns out that pretty much everything is an object. So that would still make that valid. So using object itself is not all that useful. Or I could use curly braces. But if you really want to make sure that it's an object, the thing you want to use is a record. And I can specify string keys. And I don't care about the actual property types here. So specify unknown. And now the props of type says, well, this one is independent because the string is not a record. Because a record is basically something which has keys in there. And a prop type of movie or some other type or say, if I do an inline. These number type. That's all valid. Change this to four. That's all valid. This isn't. But this isn't quite correct. Because if you look at an object in JavaScript, the keys don't always match. And I will also show you a good example here. Objects in JavaScript, the keys don't have to be strings, they can be strings, they can be numbers, or they can be symbols. So if you want to make this correct, it should be string, number, or symbol. So that's more correct. But that's a bit of a handful. It turns out there is a bit of a not very intuitive way to shortcut this. So instead of putting this in, you can do key of any, which interestingly works out to the same thing. So if I look at prop types, it says here, it's a record of string or number or symbol. If you ask me, being a bit more explicit here is well, easier to understand because of key of any. If you've never seen that, it's going to be like key of any? What's a key of any? That could be anything, right? Well, turns out it isn't. It's correct, but it works out to this. Another useful thing, if you want to create type mappings like this, you also want to test them, just like with any other code. So you kind of want to make sure that this actually causes an error. Well, I can't go and check this in because then my code won't compile. If I do, I still have my compile task running. I should, but I'm not seeing the result for some reason. Let's restart it. So now you kind of see these errors, so it's kind of like, well you can't commit that because your type check doesn't work. Well, there's some special comments you can add. Comment add, and then ts and there are a couple like you could say ts ignore, ignore the next line. But that's like just ignore it. If I save this, I won't have a compile error. Actually, I do because of something else. That's not a complete one, let's comment it out. So no errors. But now, if this for some reason doesn't error, I won't get a compile error either. So if I would remove this. It's still fine. So for something like this, a better thing is put in expect error there. If I save, it will give me a compile error because this next line does not cause an error. Cause an error. And if I put in that condition again, now found zero errors. This should error and it does error. So we're all good. Useful to type check, your actual type mappings. So please go do that.
18. Making Type Mappings Easier to Read
So I just had an interesting question. How can you filter on read-only properties? All these type mappings are nice, but it does result in one problem. They are very hard to read and understand types. Turns out that we can make those a lot easier to read. We can make those a lot easier to read by adding yet another typeMapping called Resolve. It basically just makes a copy of the original object. So now it's a lot easier to see the exact shape of the object. This type mapping works really well and can handle nested structures of objects as well. It's a nice little utility that makes reading types easier.
So I just had an interesting question. How can you filter on read-only properties? Like here where we filter on the specific target, filter on read-only. And I really don't know. Like you can do a lot. I'm, it's quite likely that it's possible, but I don't know how. So if everyone, if anyone else knows, please let us know.
All these type mappings are nice, but it does result in one problem. And those are very hard to read and understand types. If I go back to VS Code for a moment, like, um, let's actually go to the movie page. And write this slightly different. Put in the type definition like this before. Go back to using that definition. If I look at the movie select, it's kind of pretty hard to see what's going on. Then we add another bunch of things like required and pick on top of that. And if I look at the movie to select by itself, that's actually pretty readable. I can see there is, there are a bunch of properties and the whole boolean. But then if I go to the select object. Which is defined as movieToSelect which is readable. If I go here, it's like, okay, it is required of big, of some type which I can't see what it is. Which takes another default args and then there's a whole bunch of which I presume are key names. So what is this supposed to be? And the result of that is if there is something wrong, for instance, you get errors which is kind of like, well, in this case, it's relatively easy to see at the top voteAverage is missing. But quite often you go hunting through stack traces like this where you get... it's gone, where you get one description, another with requirePick, and so on. So what is going on? What is wrong? Turns out that we can make those a lot easier to read. you can make those a lot easier to read. And the interesting thing, the problem for that typeMappings make types hard to read, is to add yet another typeMapping. So it's one I came across, which Dan from the cam... What was it? Yeah, from Effective Type Scripts, right there, created. And he basically created this typeMapping, Resolve, which, kind of special case functions. But other than that, if you exclude functions, it's basically this. It basically just makes a copy of the original object. So if I copy this from the slides for a moment. And we'll put that in typeMapping and export this. I can go in and wrap all of this stuff in yet another one. resolve with lowercase e. I want to resolve that. Why does it not resolve? Now, it does. So if I hover over movies to select. It looks the same as it did before. We see all the properties with the type of boolean. But now if I hover over movie, select, we see also the exact shape it's supposed to be. I'm sorry down here, the shape it's supposed to be and the shape of this. We don't see all the type mappings, the whole definition part, which we previously used. Also, the properties are not the same. So it's not the same. The whole definition part, which we previously saw. So now it's a lot easier to see, okay, we've got an ID title overview, vector path vote counts, and we expect ID title, etc. Vote average and vote counts. So the vote average is missing, which is, of course, because I commented that. So it's a pretty simple type mapping, but it works really well. Of course, another case where well, it's just very simple generic names. Now, there's one slight problem with this which doesn't show up because these are relatively simple object. But if you get into nested objects, they become a lot harder to read, and you don't see all the nested parts. But the cool thing is you can just make this recursive. So we can say well, go resolve that type as well. So with that little change, it's recursive resolve, and even if you've got deeply nested structures of objects, it's going to show you exactly what they look like, and it will also include read only, optional stuff like that. So really useful to have and makes stuff a lot easier, and we can do the same. Where was my type mapping here? For instance, if, if I look at these are, which are hard to read, well none of these are actually hard to read Because these are still the types. They typically become hard to read if you look at an object of that type. It's not hard to read. If you, if you look at this example, you can see that it's not written in the form of a, a font. And that's one of the reasons you want to have more than one font, because the more font you have, the less. And that's, that's one of the reasons why you want to have many fonts because if you use less fonts, you lose the number of fonts. It comes, I moved my mouse a bit too much. You can see it comes up with, well, what's expected. Well, some props of type, and then it shows exactly how it's defined instead of the end result. So if I do the same here and wrap this, what is P2 in results. Now, if I check the error, it tells me exactly what's required. So. Nice little utility, let's comment this out, so it doesn't give us a compile time error all the time.
19. Using Opaque Types in TypeScript
Using opaque types in TypeScript allows us to differentiate between similar primitive data types, such as strings, and make them type checkable. This helps prevent mistakes and provides better type checking. By creating custom type mappings, we can specify the expected types for each property, such as accounts, amounts, and names. However, this approach still allows for mismatches and doesn't provide the desired type checking. To address this, we can use the opaque type, which combines a type with a read-only symbol to create a unique type. This ensures that types with similar shapes are not considered the same, providing more accurate type checking.
So please go and add this resolve and see the difference it makes on intellisense and error messages you see. So everyone is back. I was just doing a quick search for filtering read-only, but I haven't been able to find anything yet. So another useful type mapping I sometimes use is using opaque types. And the reason that's useful is that in the end, we're typically down to basic types like is a string, a house number might be a string. An amount might be a number, things like that. And it's relatively easy to make mistakes there. And you pass in, say a name where you intended to pass in an account number. And just because they're both string types, TypeScript doesn't really notice. And in fact, our code suffers from a problem like that. So if I open up the dev console for a moment, and we go to, where is my browser window? There it is. Like what I briefly did in the beginning, I added a few movies to my shopping cart. And I did checkout and I specified some name and an account number. And I did checkout. Checkout completed, no problem. And then I say, see here, a message. Checkout for 1234, which was the account number I added, and it says, charging accounts, but there we get my name. And the sentence already suggests that that should be an account number, for an amount, and then shipping movies to user Riz. Which makes sense. Now where does that come from? There is a server part. The checkout shopping cart here. And there is this function checkoutShoppingCart which is called. Which takes an account and name and an amount property, a string string number. And then prints that message basically by calling a few functions here, chargeAccount, which has charged an account, indeed with an account number for a specific amount. So even though it says account, it prints my name and then ships movie to a name and there I also received my name. So it looks like there are a couple of things wrong. Now, first of all, personally, I'm not a big fan of passing props like this because if I look at where this is actually used, it's in a dialogue on the client. You pass in and there is just a bunch of properties and it's really easy to make some kind of mismatch. And in fact, I flipped name and accounts because if I look at the definition, the parameters are account name amount. But here I did name account amount. But TypeScript says, well, this is a string, this is a string. So, string matches string. So it doesn't really matter type wise, it all matches. Of course, it's wrong and I really would like some errors. In here, I'm kind of doing the same. I've got a ship movies function here. Here's the definition and I say it expects the name of type string. But over here, I'm passing in the accounts, not the name. So, kind of like, well, easy enough to mistake to make, but hard to spot and no type checking. So that's where these opaque types are gonna help. They're gonna, let us take primitive data types, a string, numbers, Booleans, etc., and make them, well, type checkable saying, well, one string isn't equal to another string. We can differentiate between the name and the accounts number. Instead of both, having them a string will make them actually different types. Now, your first opportunity might be say, well, we'll create a type mapping then, we'll say, type accounts is a string, and we'll say, this should be an account, this should be an account, and we'll do the same with amounts. So, that amount should be an amount, that should be amount, and that should actually be a number, not a string, and that leaves us with names, so we'll create a name type, we'll make this, and those. So, now we've kind of made it more explicit, but in the end this doesn't buy us anything. If I go and check my compile step I still have no errors, and I'm still passing in here an account to something which expects a name. The reason is TypeScript compares not the actual type name, like accounts of amount, name, account, etc. But it just says, well, the name is a string, account is a string, so the shapes actually match. So we want something which is that shape. All good, but it's not. That's the error. The type I came across on the Internet a number of years back, which I've been using ever since is this opaque. It basically says, we'll give it a type, and we'll add to that this second part. This part here saying we'll add a read-only type, which is that symbol, and we'll give that something specific. If those don't match up, then the types are actually not considered to be the same. Now, this looks a bit weird, especially if you look at this. It's declare const underscore type, a unique symbol. Declare const no assignment actually, and then it's used in here. This is purely a compile-time construct. At runtime, that doesn't exist. There is no underscore type property there to check. It's whatever type you pass in. As the first argument of the generic opaque type, is what it is. You'll pass in string, number, things like that. You could theoretically pass an object here. It could be a movie. But in this case, it doesn't really make quite as much sense. But technically, that's completely legal. Let me just copy this.
20. Using Opaque Types and Type Assertions
This is a case where generic types A and B need better names. We use the opaque keyword and assign specific types to the accounts, name, and amount. This ensures that the correct types are assigned and prevents compile-time errors. Type assertions can also be used to validate the types. By creating a type assertion function, we can check if a value is of a specific type and throw an error if it is not. This provides more reliable type checking and prevents potential issues.
We'll go here. This is one of those cases where the generic types A and B, well, they're really parameters, but what's A? I always have to look. The first one is the actual type. Let's give these a better name, the type or maybe just type. B is a name we'll use. And now, we'll use opaque on these. I meant to copy that. So all of these become opaque and we have to give them some type. And so this would be the accounts. And so this will be the accounts, this will be the name. And this will be the amount. And now all of a sudden, in that ship movies, we see we expected the name but we're parsing in the accounts. And those are no longer assignable because of that underscore type being incompatible. So now we actually get a compile-time error there. So we change this to name. And that part is better. We still have another compile error though. And in our checkout shopping cart. Why can't I go there, that way, that there. Now it says okay there is an error here. And here it says well name is a string and string is not of type account. So first of all the order was wrong. So let's fix that. It was account name amount, like that. But that still doesn't actually fix the issue, because it still thinks well the account is of type string. And it expects something of type account. The definition right here. So we actually need to validate that that's the case. So there are a couple of ways we can make this code pass. And one way is, let me export these. I can say I could use a cost and say as account. Fix that and as name. And the same here as amount. So no more compile errors. But let me break the order again. Actually let me break the order. Now let me also flip these around. It's a bit easier to catch if you read it, that the order is wrong. Because now I'm saying accounts should act as a name, which looks weird. But the compile time is perfectly happy with it. So this kind of works. But it's not that reliable. So there is better way to do it. And that's with type assertions. Let me put these back in the right order. And let me get rid of these. So we get to compile errors again. And let's create a type assertion. So what does the type assertion look like? It's a function. And this has to be a function. It can't be a lambda, a fat arrow function. So export function. We'll give it some name. And they're typically called assert, the type you want to assert. So assert accounts. It takes some value, which will be the original value we expect. So in this case, an account will always need to be some kind of string. So we'll say string. And then the way this function is declared is kind of special. Instead of what you normally do is say like boolean or something like that. We're not going to do it quite like that. Now we're going to do a search. That value is an account. And then we'll add some body. So this syntax says that if this function passes then the value we pass in can be considered a type of account. And passing means that this doesn't throw an exception. So the body of this should throw an exception if that account is not a valid account or the value passed in is not a valid account. So we'll do something like if, and then we'll do, for instance, a type of value is not a string. Or, nope, that's not what I want. Then, not this for, a throw new error, invalid account.
21. Type Assertions and Strict Options in TypeScript
In TypeScript, type assertions can enhance type safety by checking values at compile time and runtime. By using type assertions, you can catch errors related to missing properties and ensure type correctness. Type assertions can be used alongside type validation functions or on their own. They provide a way to validate types and prevent potential issues. Additionally, TypeScript offers strict options that go beyond the default strict mode. These options can be useful for enforcing stricter type checking and preventing certain coding practices.
Now, if you're wondering why I'm checking for the length of four, that's because in my checkout dialogue, I'm using a schema, where is it here? And I've specified here that's the account must be exactly four long, and the name passed in must be at least two characters long, that's why I came up with this check. So now I can take this, and right now it says here that it's of type string, but if I call this assertaccount function, and I pass in that account, now all of a sudden, if I check the type here, it's considered, that's what I want to show, it's considered an account because it's passed this function without any problems, without any compile errors. So I can do the same with the name and the total amount. Let me copy this. Insert name and the value is barter or equal to two length invalids, name and this was an amount and that value should be a number, change this to number, that's actually enough. Did I not catch it because we are checking for value of greater than 2 and then we're throwing error. Should the value of the name less than 2? Yes, indeed, you're correct. I've got it. Yeah. So, if it's 2 or larger, it's valid. So we should throw an error if it's not valid. So now I can go back here and basically do the same for the amounts and the name. Name and the Asserts. amount, so that should be total amount. And that should be data.name. So now it's safe, the way it checks everything, and it's not just... where's my code? Did I still have a typo somewhere? I should not have put these in here. That's another next feature... new next feature along with client or server components, you can declare code like this with a new server, so it has to run on the server. Which makes everything asynchronous and we don't actually want to go to the server for that, so let's move them to the type mappings file. This doesn't have a user server, so that can be client or server. And do the same with those types. So those need to be imported from somewhere else now. At all missing imports. That should fix all of those. And that should fix those. So now it works again. I can add something to the cart, do a checkout. And everything should be fine. Let's go to the Dev step. Now we indeed see checkout for modele, checkout account. But if for some reason I still have those wrong, let me actually make an intentional error. So I'll pass in that name as the first again. And then, say. We'll assert that the name is an account and the account is a name, which is a bit weird. Just to get it to pass compilation. Now the nice thing is, you'll see that it actually has run time implications. So I go to checkout again. On the checkout, you see now we get the invalid account because that error is wrong, because it actually checks the account against the name value and the other way around. So we get both compile and run time type safety. Let me undo this, so. It's good again. Let me just double check. Yeah, so we're good again. So it's pretty neat. Things like this can make type safety a lot better. Not sure what's popping up there. So again, a relatively simple type mapping, but it's adds a lot of type safety. Type assertions like this. Quite often you'll see those type assertions along with type validation functions, which just return true or false if something matches, which I've actually got in the slides. But you don't have to use both. Like over here, I've got a type check. This function isAccount which turns true if the value is an account, and then the type assertion actually uses that. But if you don't need both then just write the assertion like I just did. In this case, I left the arguments from the clients as simple strings and just used them on the backend, which works just as well. Fixed the shopping cart. And with that, let's go and do this exercise. Let's do one more exercise, because I think that's another useful one to know. And then we'll wrap things up, because we covered most of the ground anyway. So the last thing I want to take a look at is more strict features. Now, most people that use TypeScript are familiar with the strict option. And even if you're not, if I go to the tsconfig file, like most times you generate some projects, whether it's with Next.js or something else, you'll automatically get strict set to true. So you'll, even if you're not aware of it, you typically run with strict. But most people think that's, well, I'm running with strict, so TypeScript is in the strict mode and that's it. But it turns out that TypeScript can be stricter than strict. There is a whole bunch of settings. And as far as I know, this is the complete list. But maybe there is actually another one which is not affected by setting strict to true. And there's something like allow unreasonable code or allow unused labels, which you might argue, well, not so sure I need to set that. Especially allow unreasonable code is awkward because now you can't just shortcut a function by saying return true at the top if you want to just skip out over some code to debug things, etc.
22. Preventing Undefined Errors in TypeScript
Enabling the 'no unchecked index access' option in TypeScript helps prevent errors caused by accessing undefined properties. By enabling this option, the TypeScript compiler detects potential undefined values and provides more accurate type checking. In a specific example, accessing the 'topMovie' property caused an error because the movie array was empty, resulting in undefined. By conditionally rendering the 'topMovie' property only when it exists, we can prevent the error. It is recommended to enable the 'no unchecked index access' option in all projects to improve type checking and prevent common errors.
So not a big fan of those, but you can use it. But there is one here. And that's no unchecked index access, which is really useful. And let me show you an example where I would use that. Like I mentioned in the beginning, we can filter movies. We can show all or we can filter them by genre. And I told you don't use horror because we'll get into a horror situation. And right here we're running into an error, cannot read property, undefined reading title. So something is wrong here, but again, type checking is all fine, or at least it was, yeah, no errors. And that happened somewhere in that movie page. And if I scroll down a bit, the error happens right here with that top rated movie. And if I hover over that top movie, it says it's top movie, pick of whatever. But no reason to seem it should ever be undefined. And like right here it just picks the first item for movies. But I just want to show the bottom of this if I can scroll down and see it's not movie, top movie. Scroll down like there is no or undefined here. There is nothing optional like it defines that top movie is always movie object. But is that true? We're taking the first element of a movie array. What happens if that array is empty? What will top movie be at runtime? It's going to be undefined. Because retrieving something outside of the valid bounds of an array is perfectly legal in JavaScript, so it's legal in TypeScript. So if this movie array is empty, indexing 0 means the first element. So we actually get undefined back, which is exactly what's causing this error. This genre, horror, doesn't have any movies in there. And that's where that option comes in, the no unchecked index access. So let's enable that. No unchecked index access. It defaults to false, but we'll set it to true. I'll save, and immediately the TypeScript compiler detects, OK, there is one error. And now it actually says, well, over where I was just a minute ago, no errors. Now it actually says, well, top movie is possibly undefined. And if I hover here over the type, it looks the same. It looks the same pic of that whole type, but now, right at the bottom you see. Try again. You see here, or, undefined. So now, we actually know that it can be potentially undefined, because we have to check first. Now, one thing we could do is, have a loop, have a condition here..movies.length is larger than 0 And in that case, topMovie is guaranteed. If it will show me the type. It's still, well, interesting. It still actually says it's possibly undefined. Even though now we can only get in here. If we have more than 0 movies. But anyway, that wouldn't really work in this case. Because now that's variable topMovie here isn't valid. It's only declared inside of the scope. So in this case, the solution is actually a little different. We only want to render this conditionally. So we'll make this an expression and say. If we have topMovie. Then, no question mark, we want to render this. And if not, we want to render nothing maybe. So we only render the top rated movie if there is something. And now we get an empty page, but with something else. It actually shows that top rated movie. Or we could potentially say, we want to render something else. Say. Remove the null. There are no movies or something like that. So. Now at least it's a little friendlier because there is a message there. Or you might potentially want to use. Conditional there like. Render the movie title or render nothing. Different ways to solve the problem but at least now we get a compile time error and when we don't check and because we check that error goes away. So I highly recommend enabling this no unchecked index access in all your projects. This makes the type checking a lot better because this is a relatively common error. Where you think a type is defined as something but it's actually something or undefined. Happens quite a lot. So the no unchecked index access Not actually going to do the exercise because we're past the end of time. I had some more stuff about read only and deep read only. But I'm going to go to the conclusion.
23. Summary of Typemappings and Opaque Types
A lot of useful stuff we saw with typemappings. Give the Typescript compiler more information with simple typemappings. Read-only and deep read-only are great utilities. Importing large JSON files can slow down the Typescript compiler. Use the strict feature with no unchecked index access. Compile your code and update generated types. Opaque types with assert functions exist at compile time and runtime. They enhance type safety and prevent errors. At runtime, the assert functions check the type and throw an error if it's invalid.
A lot of useful stuff we saw with typemappings. There's a lot you can do there. It can be a little tricky to get started with, but it's an investment which pays off in my opinion. So I would highly recommend doing that. You can give the Typescript compiler a lot more information with a handful of relatively simple typemappings.
Read-only is another one, standard one which you can use. In the sample code you'll find the deep read-only or recursive read-only, which is another great utility to add. If you're importing large JSON files, I would also recommend taking a look at that last item where you can make the Typescript compiler a lot faster because if you import JSON files, the Typescript compiler actually parses the complete JSON file in order to determine the type returned. That can be slow, but only with larger JSON files.
So, lots of useful stuff. Strict feature with no unchecked index access, highly recommended. Make sure you compile your code. Make sure you update your generated types if you're using something like GraphQL type generation or OpenAPI type generation, stuff like that. So with that I'd like to wrap it up. It's past due time, so I'm going to take a break. If there are any questions now would be a good time to unmute your mic and ask them. If not, thank you for being here. I hope it was useful. My apologies for my coughs occasionally and being not completely 100%, but nothing much I can do about that, bit unfortunate. I was getting rather warm as well here, feeling better now. But any questions before we wrap up. Okay, then I have one about the Opaque types which we declared with the assert functions. We didn't use actually in the assert function these read-only properties of our account name. Should we connect it, or it was just separate?
No, the thing... where did it go here? We defined an Opaque type here, account, which is of a type string. And it has this additional name of account, which is stored in here. This only exists at compile time, it doesn't exist at runtime. At runtime, that's never added, so it's never there. These assert functions, they were both at compile time, and at runtime. At compile time, it's basically TypeScript saying, well, if we get past this function, so... where was I using them, here? So, we pass in a string here, but if we get past this function, there was no exception there, then we can consider that this account passed in was of the type account. Otherwise, it should have thrown... At runtime, this function actually executes, so at compile time it doesn't execute because there is no value yet, it just considers, well, if this passes, then this must be true. At runtime, this executes, and it will actually check, well, is the value passed in of type string? Is the length of four, and if that's not the case, it will at runtime actually throw an error and cause the program to crash or to catch the error or something. And in case you were wondering, I've got value defined as string here and then at runtime I'm again checking whether it's of type string. That's again the compile time versus runtime. If I do an assert account and I pass in a number, that's impossible to be valid. If the TypeScript compiler knows the value I pass in is a number, so I do something like assert accounts 12. The TypeScript compiler is going to put squigglies in here and say well it's impossible for this to be valid because it's number, but if I want to be sneaky, I can do this. Castit as any. Now the TypeScript compiler, and you probably won't do it like this, but it will probably be some any type being passed in. The TypeScript compiler has no information about whatever is being passed in. So it's just a run time check and even though this has a string at run time it could still be a number. Or potentially if you're building a library which is used from no type check, you could pass in a number or a boolean or whatever instead of a string. So that's why it's kind of double. This is for compile time, this part is for run time.
Okay, yeah, that's explains. That makes great sense. Thank you. And that's it then I guess. Thank you all for attending. Sorry it was a bit messy. I'm not quite feeling all that well. But I hope it's still useful and maybe you'll follow another workshop of me sometime.
Comments