text: Introduction
  These notes should help in better understanding
  TypeScript and might be helpful when needing to lookup up how to leverage TypeScript in a specific situation. All examples in this post are based on TypeScript 3.7.2.
  Basics
  When working with TypeScript, we want to rely on the defined types throughout the application.
  But can we always rely on these types? There are cases where we can't guarantee that these types are valid at compile time. Let's think about some possible cases where this might apply.
  When working with external files that we might load at runtime, or a simple API fetch. We can't guarantee that what we expect is actually the expected type at runtime. There might be cases where we control the complete application cycle and can assume that the expected types apply, but even then, we're dealing with an side-effects, that we want to handle safely as possible.
  Let's see an example where we assume a specific type is actually valid, but where we might run into problems at runtime.
  type Theme = "basic" | "advanced"; type User = { id: number; name: string; points: number; active: boolean; settings: { configuration: { theme: Theme; }; }; };
  User type defines a specific user shape, nothing too special so far. In our application we will probably be retrieving this user object via an endpoint and use this user object to later update some properties.
  Somewhere in our app, we want to enable to change the user settings, to make the profile configurable.
  changeTheme is a simplified function that accepts a possible theme and updates the user settings.
  const changeTheme = (user: User, theme: Theme): User => { return { ...user, settings: { ...user.settings, configuration: { ...user.settings.configuration, theme } } }; };
  The
  user object has to come from somewhere, so let's assume we have a function that takes care of loading that user.
  const loadUser = () => { fetch("") .then(response => response.json()) .then((user: User) => state.saveUser(user)) .catch(error => { console.log({ error }); }); };
  Furthermore we might want to keep state of that user inside the client, so we call the
  saveUser function to keep track of the object.
  type State = { user: User | undefined; saveUser: (user: User) => void; getUser: () => User | undefined; }; const state: State = { user: undefined, saveUser: (user: User) => { state.user = user; }, getUser: () => { return state.user; } };
  TypeScript will not complain. Calling the
  json function on the response object returns an
  Promise<any>, so there is no guarantee that we are dealing with a
  User type. The following code will compile as well. If you recall
  changeTheme expects a
  User type.
  const user = state.getUser(); if (user) { const result = changeTheme(user, "advanced"); }
  When calling
  changeTheme with the following fetch result, we will be greeted with an error at runtime. We expected the property to have the
  settings key, but it actually returned
  setting.
  const user = { id: 1, name: "Test User", points: 100, active: true, setting: { configuration: { theme: "basic" } } }; changeTheme(user, "advanced"); // Error! // Uncaught TypeError: Cannot read property 'configuration' of undefined at changeTheme
  This example might be very simplified, but it should show that we can't fully rely on the data coming from an external source. We could be more defensive when working with
  user, which we should be doing anyway, but it also means that we can rely on these types even less. Adding a check will prevent the previous error.
  if (!user || !user.settings || !user.settings.configuration) { return user; }
  We have seen that we need to be more restrictive when working with external data and relying TypeScript defined types, but there might be a more convenient way to handle side-effects when working inside a TypeScript codebase.
  Advanced
  If you have worked with
  Elm or
  ReasonMl before, you might have noticed that there is a way to encode and decode any JSON data. This is very valuable as we want to ensure that JSON data we are getting has the expected type. TypeScript by default doesn't offer any runtime JSON validators, but there are a couple of possible solutions to solving that problem in TS land. We will use one of these libraries to show how we can make our types more reliable.
  io-ts is a library written by Giulio Canti that is focused on runtime type validations. We will not get too deep into the details and capabilities of this library, as the official
  README covers most of the basics. But it's interesting to note, that the library offers a large range of so called
  codecs, which are runtime representations of specific static types. These codecs can then be composed to build larger type validations.
  Codecs enable us to encode and decode in and out data and the built-in
  decode method that every code exposes, returns an
  Either type, which represents success (Right) and failure (Left). This enables us to decode any external data and handle success/failure specifically. To get a better understanding let's rebuild our previous example using the
  io-ts library.
  import * as t from "io-ts"; const User = t.type({ id: t.number, name: t.string, points: t.number, active: t.boolean, settings: t.type({ configuration: t.type({ theme: t.keyof({ basic: null, advanced: null }) }) }) });
  By combing different codecs like
  string or
  number we can construct a
  User runtime type, that we can use for validating any incoming
  user data.
  The previous basic construct has the same shape as the
  User type we defined previously. What we don't want though, is to redefine the
  User as a static type as well.
  io-ts can help us here, by offering
  TypeOf which enables user land to generate a static representation of the constructed
  User.
  type UserType = t.TypeOf<typeof User>;
  This gives us the exact same representation we defined in the beginning of this post.
  type UserType = { id: number; name: string; points: number; active: boolean; settings: { configuration: { theme: 'basic' | 'advanced'; }; }; };
  To clarify the concept, let assume we have a
  user object and want to validate if it has the expected shape.
  io-ts also comes with
  fp-ts as a peer dependency, which offers useful utility functions like
  isRight or
  fold. We can use the
  isRight function to check if the decoded result is valid.
  const userA = { id: 1, name: "Test User", points: 100, active: true, settings: { configuration: { theme: "basic" } } }; isRight(User.decode(userA)); // true const userB = { id: 1, name: "Test User", points: "100", active: true, settings: { configuration: { theme: "basic" } } }; isRight(User.decode(userB)); // false
  Now that we have some basic understanding of how
  io-ts functions, let's revisit the original
  loadUser function and decode the data before we pass it around inside our application. But before we start refactoring let's take a look at one more useful functionality, that will help us when working with the
  Either type, that the decode returns.
  fold enables us to define a success and failure path, check the following example for more clarification:
  const validate = fold( error => console.log({ error }), result =>console.log({ result }) ); // success case validate(User.decode(userA)); // failure case validate(User.decode(userB));
  Using
  fold enables us to handle valid or invalid data when calling our fetch functionality. The
  loadUser function could now be refactored to handle these cases.
  const resolveUser = fold( (errors: t.Errors) => { throw new Error(`${errors.length} errors found!`); }, (user: User) => state.saveUser(user) ); const loadUser = () => { fetch("") .then(response => response.json()) .then(user => resolveUser(User.decode(user))) .catch(error => { console.log({ error }); }); };
  As we can see from the above code, we might handle any incorrect representation by throwing another error. This prevents the data from being passed around in our application. There are more improvement we can make here. Right now, we're being very specific in how we're handling the
  user decoding. There might be an opportunity to write a general function that handles any promise based data.
  const decodePromise = <I, O>(type: t.Decoder<I, O>, value: I): Promise<O> => { return fold<t.Errors, O, Promise<O>>( errors => Promise.reject(errors), result => Promise.resolve(result) )(type.decode(value)); };
  Our
  decodePromise function handles any input data based on a defined decoder and then returns a promise, based on running the actual decoding operation.
  const loadUser = () => { fetch("") .then(response => response.json()) .then(user => decodePromise(User, user)) .then((user: User) => state.saveUser(user)) .catch(error => { console.log({ error }); }); };
  There are more improvements we could make, but we should have a basic understanding of why it might be useful to validate any external data at runtime.
  io-ts offers more features like error reporters or handling recursive and optional types. Furthermore there are libraries like
  io-ts-promise that provide more features and useful helpers, the above
  decodePromise, for example, is available in a more advanced variant via
  io-ts-promise.
  Links
  If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif
  Discussion (1)
  Nice!, I miss that on TypeScript too much...,
  on Nim you have run-time checks, side effects tracking, immutability, pure-functions, and JSON Types.