> ## Content Index
> Fetch the complete content index at: https://www.mrstebo.co.uk/llms.txt
> Use this file to discover other available public pages before exploring further.

# Awesome Typescript I Never Knew @ Manc.JS
- URL: https://www.mrstebo.co.uk/awesome-typescript-i-never-knew-manc-js/
- Published: 2023-07-21T06:30:18.000Z
- Updated: 2023-07-21T06:30:17.000Z
- Author: Steven Atkinson
- Tags: Typescript, Meetups, Community, Knowledge Sharing, Software Development

Recently, I had the pleasure of attending the [Manc.JS Meetup](https://www.meetup.com/mancjs/events/294235265?ref=mrstebo.co.uk) where Chris Dell gave a comprehensive overview of TypeScript. This blog post aims to recapitulate some of the most interesting points discussed.

## Defining Types on Variables

TypeScript enhances JavaScript by introducing type annotations. By defining types on variables, you can avoid common errors resulting from unexpected variable values. You declare the type of a variable using a colon after the variable name:

```typescript
let name: string;
name = 'John'; // Ok
name = 123;    // Error: Type 'number' is not assignable to type 'string'.

```

## Type Inference

![](https://storage.ghost.io/c/bd/d8/bdd88a76-8cec-4cb3-885c-315b9aa4afd5/content/images/2023/07/image-49.png)

TypeScript's type inference feature means that you don't always have to explicitly annotate the variable's type. If you assign a value to the variable when declaring it, TypeScript will automatically infer the type:

```typescript
let age = 20;   // TypeScript infers that 'age' is a number.
age = 'twenty'; // Error: Type 'string' is not assignable to type 'number'.

```

## Literal Types

A literal type is a type that represents exactly one value. These are often used in conjunction with union types to represent a finite set of possible values. Here's an example:

```typescript
type WindowStates = "open" | "closed" | "minimized";

let currentWindowState: WindowStates = "open";

currentWindowState = "closed"; // This is fine

currentWindowState = "maximized"; // Error: Type '"maximized"' is not assignable to type 'WindowStates'.

```

In the above example, `WindowStates` is a literal type that can have one of three specific string values: `"open"`, `"closed"`, or `"minimized"`. Any attempt to assign a different string value to a `WindowStates` variable will result in a compile-time error.

Number and boolean literals can also be used:

```typescript
type Zero = 0;
let zeroValue: Zero = 0;

type TrueFlag = true;
let flag: TrueFlag = true;

```

In the above, `Zero` can only ever hold the value `0`, and `TrueFlag` can only ever hold the value `true`. Attempting to assign any other value to these variables will result in a TypeScript error.

## The 'any', 'unknown', and 'never' Types

![](https://storage.ghost.io/c/bd/d8/bdd88a76-8cec-4cb3-885c-315b9aa4afd5/content/images/2023/07/image-48.png)

The `any` type is a powerful feature in TypeScript, allowing you to opt-out of type-checking. However, it should be used sparingly as it circumvents the type system.

```typescript
let value: any = 5;
value = "now I am a string";
value = { field: "and now I am an object" };

// No errors are caught here, but assigning a number to a string and 
// an object to a string would cause runtime errors.

```

The `unknown` type is similar to `any`, but safer. It's a type-safe counterpart of `any` that forces you to perform some form of type-checking before you can use the values.

```typescript
let unknownValue: unknown = 5;
unknownValue = "now I am a string";
unknownValue = { field: "and now I am an object" };

// TypeScript Error: Object is of type 'unknown'.
// console.log(unknownValue.field); 

if (typeof unknownValue === 'object' && unknownValue !== null) {
  // TypeScript knows that `unknownValue` is an object here, so there's no error.
  console.log(unknownValue.field);
}

```

The `never` type is used for values that never occur. For instance, a function that always throws an error never returns a value:

```typescript
function alwaysThrows(): never {
  throw new Error("I always throw, I never return a value!");
}

// Because `alwaysThrows` never returns, it is a compile-time error to try to
// use its return value.
// TypeScript Error: Unreachable code detected.
// let x = alwaysThrows();

```

## The `type` Operator and Union Types

The `type` operator is used to create custom types. Union types allow you to combine multiple types into one using the `|` operator, useful when a variable can be more than one type:

```typescript
type StringOrNumber = string | number;

let data: StringOrNumber;
data = 'Hello'; // Ok
data = 42; // Ok

```

## Intersection Types

![](https://storage.ghost.io/c/bd/d8/bdd88a76-8cec-4cb3-885c-315b9aa4afd5/content/images/2023/07/image-46.png)

Intersection types are a way of combining multiple types into one. This allows you to mix together several structures to create a new type. It's a way of composing types and adding together existing types to get a single type that has all the features you need.

In TypeScript, you can use the `&` operator to create an intersection type. Here's a simple example:

```typescript
type Employee = {
  id: number;
  name: string;
};

type Manager = {
  employees: Employee[];
};

// The ManagerEmployee type is an intersection of Employee and Manager
type ManagerEmployee = Employee & Manager;

let managerEmployee: ManagerEmployee = {
  id: 1,
  name: 'John Doe',
  employees: [{
    id: 2,
    name: 'Jane Smith'
  }]
};

```

In this example, `ManagerEmployee` is an intersection of `Employee` and `Manager`. A `ManagerEmployee` has all the properties of `Employee` (i.e., `id` and `name`), as well as all the properties of `Manager` (i.e., `employees`).

Note that intersection types can be a bit tricky when combining types with properties that have the same name but different types. In such cases, TypeScript will only allow you to assign values that are assignable to both types. For example:

```typescript
type A = {
  foo: number;
};

type B = {
  foo: string;
};

type C = A & B;

let c: C = {
  foo: ???  // There's no value that is both a number and a string!
};

```

In this case, `C` is not a usable type, because there's no value that can be both a `number` and a `string`. So while intersection types are very useful for composing types, be aware of potential pitfalls when intersecting types with overlapping properties.

## The `as const` Syntax

The `as const` syntax in TypeScript is used to create a "read-only" version of a literal, where all the properties of an object literal become `readonly`, arrays become read-only tuples, and string, number, or boolean literals don't get widened to their more general type (string, number, boolean). This can be useful when you want to prevent mutations to a literal value.

Here's an example of using `as const`:

```typescript
const car = {
  brand: 'Toyota',
  model: 'Camry',
  year: 2020,
} as const;

car.brand = 'Honda'; // Error: Cannot assign to 'brand' because it is a read-only property.

```

In the above code, the `as const` makes all properties of the `car` object `readonly`, so any attempts to modify the properties will result in a compile-time error.

Another powerful use case of `as const` is to define an exact set of string or numeric literals that a variable can be set to. This allows you to leverage TypeScript's type-checking to ensure that variables can only be set to an expected set of values:

```typescript
const COLORS = ['red', 'green', 'blue'] as const;

type Color = typeof COLORS[number]; // 'red' | 'green' | 'blue'

let myColor: Color;

myColor = 'red'; // Ok
myColor = 'purple'; // Error: Type '"purple"' is not assignable to type 'Color'.

```

In this example, `COLORS` is a read-only tuple of string literals, and `Color` is a type that can only be one of those specific string literals. The `as const` assertion ensures that TypeScript uses these exact values ('red', 'green', 'blue') for the `Color` type, rather than just string.

## Type Narrowing

![](https://storage.ghost.io/c/bd/d8/bdd88a76-8cec-4cb3-885c-315b9aa4afd5/content/images/2023/07/image-50.png)

Type narrowing refers to refining the type of a variable within a certain scope, based on certain checks or conditions.

An example of type narrowing would be using a `typeof` check in an `if` statement:

```typescript
let value: string | number = Math.random() < 0.5 ? "Hello" : 42;

if (typeof value === "string") {
  // In this block, TypeScript knows `value` is a string.
  console.log(value.toUpperCase()); // No error
} else {
  // In this block, TypeScript knows `value` is a number.
  console.log(value.toFixed(2)); // No error
}

```

## Type Guards

Type guards are expressions that perform a runtime check that guarantees the type in some scope. They are generally used in the form of functions.

```typescript
function isString(test: any): test is string{
  return typeof test === "string";
}

function example(foo: any){
  if (isString(foo)) {
    console.log(foo.length); // string function
  } 
}

```

In the `isString` function, `test is string` is a type predicate. If this function returns `true`, TypeScript knows that `test` must be a `string` in any scope where this function returned `true`.

## Discriminated Unions

Discriminated Unions (also known as tagged unions or algebraic data types) is a pattern in TypeScript which makes it possible to handle scenarios where an object could be of multiple types. Each type in the union has a common literal type property (the "discriminant") that you can check to see which type of object you have.

Here's an example:

```typescript
type Circle = {
  kind: "circle";
  radius: number;
};

type Square = {
  kind: "square";
  sideLength: number;
};

type Shape = Circle | Square;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      // TypeScript knows this is a Circle
      return Math.PI * shape.radius ** 2;
    case "square":
      // TypeScript knows this is a Square
      return shape.sideLength ** 2;
  }
}

```

In this example, `Shape` is a discriminated union of `Circle` and `Square`, discriminated by the `kind` field. In the `getArea` function, TypeScript can automatically narrow down the type of the `shape` parameter inside each case of the switch statement.

## Exhaustiveness Checking in Discriminated Unions

![](https://storage.ghost.io/c/bd/d8/bdd88a76-8cec-4cb3-885c-315b9aa4afd5/content/images/2023/07/image-51.png)

You can use a utility function to ensure all cases in a discriminated union are handled, a process called exhaustiveness checking. This is a great way to leverage the type system to prevent runtime errors.

Consider the following `Shape` discriminated union:

```typescript
type Circle = {
  kind: "circle";
  radius: number;
};

type Square = {
  kind: "square";
  sideLength: number;
};

type Triangle = {
  kind: "triangle";
  base: number;
  height: number;
};

type Shape = Circle | Square | Triangle;

```

### Good Example (exhaustiveness checking)

Here's an example of a function that correctly uses exhaustiveness checking:

```typescript
function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    case "triangle":
      return 0.5 * shape.base * shape.height;
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

```

In this function, if you forget to handle a shape (e.g., a `Triangle`), TypeScript will raise a compile-time error on the `_exhaustiveCheck` line: "Type 'Triangle' is not assignable to type 'never'."

```typescript
function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      // Type 'Triangle' is not assignable to type 'never'.
      return _exhaustiveCheck;
  }
}
```

### Bad Example (missing exhaustiveness checking)

Now let's add in the `Triangle` shape:

```typescript
type Triangle = {
  kind: "triangle";
  base: number;
  height: number;
};

type Shape = Circle | Square | Triangle;

```

Because we do not have the default case which performs the exhaustiveness check, we have:

```typescript
function getAreaBad(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    // Oops, we forgot to handle Triangle!
  }
  // This won't give a compile-time error even though we forgot to handle Triangle.
}

```

In this case, TypeScript won't catch the error of forgetting to handle the `Triangle` type in the switch statement. This is why exhaustiveness checking is so important in discriminated unions; it helps ensure all cases are handled.

## Generic Functions

In TypeScript, generics are a way to create reusable components that work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

In this case, we'll create a generic function that works with a more complex object structure.

Let's assume we have a generic `Response` type which represents a response from a server, which has a `status` property and a `data` property, where `data` can be any type:

```typescript
type Response<T> = {
  status: 'success' | 'failure';
  data: T;
}

```

We could then create a function that logs out the data from a successful response. To ensure it can work with any type of data, we'll make it a generic function:

```typescript
function logSuccessData<T>(response: Response<T>): void {
  if (response.status === 'success') {
    console.log('Data:', response.data);
  } else {
    console.log('No data - request failed');
  }
}

```

In this example, `logSuccessData` is a generic function that can work with a `Response` object with any type of `data`. Here's how you could use it:

```typescript
const numberResponse: Response<number> = {
  status: 'success',
  data: 42
};

logSuccessData(numberResponse); // Logs: 'Data: 42'

const stringResponse: Response<string> = {
  status: 'failure',
  data: 'Something went wrong'
};

logSuccessData(stringResponse); // Logs: 'No data - request failed'

```

In both calls to `logSuccessData`, TypeScript automatically infers the type for `T` based on the `Response` object that is passed in. With `numberResponse`, `T` is inferred as `number`, and with `stringResponse`, `T` is inferred as `string`. This way, the `logSuccessData` function can be used with a `Response` containing any type of data.

## Opaque Types

![](https://storage.ghost.io/c/bd/d8/bdd88a76-8cec-4cb3-885c-315b9aa4afd5/content/images/2023/07/image-52.png)

The `ts-essentials` package provides a lot of useful type helpers for TypeScript, and one of them is the `Opaque` type.

The `Opaque` type allows you to create opaque types in TypeScript, which are a way to prevent illegal use of values by hiding their internal structure and only allowing operations that are defined for that type.

Here's an example where we define `UserID` and `ProductID` as opaque types, to ensure they're not used interchangeably:

```typescript
type UserID = Opaque<number, "UserID">;
type ProductID = Opaque<number, "ProductID">;

function assignProductToUser(userId: UserID, productId: ProductID) {
  // ...
}

// To create a UserID or ProductID, you have to cast a number to the appropriate type:
let userId: UserID = 1 as UserID;
let productId: ProductID = 101 as ProductID;

assignProductToUser(userId, productId);

```

In this example, a `UserID` is just a number, and a `ProductID` is also just a number - but TypeScript treats them as different types, because they're defined as different opaque types. This means you can't accidentally pass a `ProductID` to a function that expects a `UserID`, or vice versa.

This is a powerful feature that can help prevent bugs by enforcing stricter type checking. However, be aware that TypeScript only enforces this at compile time. At runtime, a `UserID` and a `ProductID` are both just numbers.

One drawback of the `Opaque` type from `ts-essentials` is that you have to cast a number to a `UserID` or `ProductID` to create values of these types. This can be a bit verbose, but it's necessary to ensure type safety. If you find this to be a problem, you might want to define helper functions to create `UserID` and `ProductID` values more easily.

## Zod for Validating Objects

Zod is a library for creating and validating schemas. It's a powerful tool when working with TypeScript, as it can infer the types from the schemas you define, ensuring type safety. It provides a set of functions to define and validate data of any shape, while providing excellent TypeScript integration. 

Here is a schema for a simple user object:

```typescript
import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

```

In this example, `UserSchema` is a Zod schema that describes a user object. It specifies that a user object should have `id` as a number, `name` as a string, and `email` as a string that should be a valid email address.

The `User` type is then inferred from `UserSchema`. This gives you a TypeScript type that matches the shape of the schema. With Zod, you don't need to manually keep your TypeScript types and Zod schemas in sync - they're automatically linked!

Now, you can use the `parse` or `safeParse` method of `UserSchema` to validate data:

```typescript
// Suppose we have some data that should be a user object:
const data = {
  id: 1,
  name: 'John Doe',
  email: 'john@example.com',
};

// To validate this data against the schema, use the parse method:
const user = UserSchema.parse(data);

// If the data doesn't match the schema, parse will throw an error:
try {
  const user2 = UserSchema.parse({ ...data, id: 'not a number' });
} catch (err) {
  console.log(err);  // This will log an error because 'id' should be a number
}

// If you don't want to use try/catch, you can use safeParse instead:
const result = UserSchema.safeParse({ ...data, id: 'not a number' });

if (!result.success) {
  console.log(result.error);  // This will log an error because 'id' should be a number
}

```

This makes Zod a powerful tool for validating incoming data in TypeScript. It lets you ensure that data matches a certain shape, and it provides useful error messages when data doesn't match the schema. Plus, it integrates tightly with TypeScript, so you can get compile-time type safety and auto-completion for your schemas.

## The `satisfies` Keyword

The `satisfies` keyword is a unique TypeScript feature used within type guards to determine if an object conforms to a specific type. This is an alternative to using the zod package for object validation. Here is an example:

```typescript
type Colors = "red" | "green" | "blue";

type RGB = [red: number, green: number, blue: number];

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
} satisfies Record<Colors, string | RGB>;

const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();
```

## Prisma ORM and TypeScript

![](https://storage.ghost.io/c/bd/d8/bdd88a76-8cec-4cb3-885c-315b9aa4afd5/content/images/2023/07/image-53.png)

Chris demonstrated the powerful interaction between Prisma ORM and TypeScript, showcasing how type inference can help when working with objects returned from a database query.

Here is an example that shows how the Prisma Client uses TypeScript's type inference capabilities to ensure type safety in database operations.

Consider the following schema defined in the Prisma Schema Language:

```prisma
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
}

```

We can use Prisma Client to perform database operations on the `Post` model. After fetching data from the database, the resulting objects will be of the type inferred from the model, allowing us to safely access the properties of these objects:

```typescript
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
  const post = await prisma.post.create({
    data: {
      title: "Type Safety with Prisma",
      content: "Prisma Client provides excellent type safety.",
      published: true,
    },
  });

  console.log(post.id); // Okay
  console.log(post.title); // Okay
  console.log(post.content); // Okay
  console.log(post.published); // Okay
  console.log(post.nonExistentProperty); // TypeScript error: Property 'nonExistentProperty' does not exist on type 'Post'.
}

main()
  .catch((e) => {
    throw e;
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

```

In the above example, we use `prisma.post.create()` to create a new post. The resulting `post` object is of the type `Post`, as inferred from the Prisma schema. Thanks to TypeScript's static type checking, we can safely access properties of the `post` object knowing that they exist. If we try to access a non-existent property, TypeScript will raise a compile-time error.

## Templated Strings at Compile Time

The session also explored an interesting example of using templated strings at compile time. This could be a game-changer for managing localisation dictionaries.

Let's consider a simple localization dictionary. In TypeScript, we can enforce that only valid keys are used with the dictionary, and that the expected arguments for each template are provided.

Consider this localization dictionary:

```typescript
type Dictionary = {
  welcome: (username: string) => string,
  goodbye: () => string,
};

const dictionary: Dictionary = {
  welcome: (username: string) => `Hello, ${username}!`,
  goodbye: () => 'Goodbye!',
};

```

In the example above, our dictionary has two keys, 'welcome' and 'goodbye'. The 'welcome' key corresponds to a function that takes a username and returns a string. The 'goodbye' key corresponds to a function that takes no arguments and returns a string.

We can use this dictionary safely, with TypeScript ensuring we provide the correct arguments:

```typescript
console.log(dictionary.welcome('Alice'));  // Hello, Alice!
console.log(dictionary.goodbye());  // Goodbye!

// TypeScript will give us an error if we use the dictionary incorrectly:
console.log(dictionary.welcome());  // Error: Expected 1 arguments, but got 0.
console.log(dictionary.goodbye('Alice'));  // Error: Expected 0 arguments, but got 1.

```

In this way, TypeScript can help us to create safer localization dictionaries, by checking the keys and arguments at compile time. This prevents us from making mistakes like using a non-existent key or providing the wrong arguments to a template string.

*Chris' example was a lot better than this - as it used a lot of inferring types to convert `{{value}}` that mapped to an object with `value` as the key.*

## AssemblyScript

![](https://storage.ghost.io/c/bd/d8/bdd88a76-8cec-4cb3-885c-315b9aa4afd5/content/images/2023/07/image-54.png)

Chris also introduced us to WebAssembly using [AssemblyScript](https://www.assemblyscript.org/?ref=mrstebo.co.uk). For those unfamiliar, AssemblyScript is a variant of TypeScript (a subset, to be more precise), which allows developers to write strictly typed TypeScript code and compile it directly to WebAssembly. WebAssembly, or wasm, offers a high-performance run-time environment within the browser, bringing near-native speed to web applications.

To showcase the power of AssemblyScript, Chris chose to walk us through a fascinating application: rendering the Mandelbrot set. The Mandelbrot set, named after the mathematician Benoit Mandelbrot, is a complex and beautiful fractal that's often used as a benchmark for performance because of its computationally intensive nature.

Chris showed us how we could use AssemblyScript to write the logic for generating the Mandelbrot set and then compile that to WebAssembly. He then contrasted this with a JavaScript implementation, demonstrating the stark differences in performance.

The example highlighted how WebAssembly, when used for computationally heavy tasks, can dramatically improve the performance of web applications. While JavaScript is highly optimized and performant for a wide range of applications, the speed benefits of WebAssembly come into play when dealing with tasks that involve heavy computations, like rendering the Mandelbrot set.

What's particularly enticing about AssemblyScript, as Chris demonstrated, is the familiarity it provides for TypeScript developers. Its syntax and type system are similar to TypeScript's, making it an accessible entry point for JavaScript and TypeScript developers looking to leverage the benefits of WebAssembly without having to learn a more complex language like C or Rust.

## Conclusion

In summary, the Manc.JS meetup provided insightful and practical knowledge on TypeScript. Chris Dell's in-depth presentation showcased how TypeScript's robust feature set can significantly enhance code quality, maintainability, and predictability. I learnt a good few things that I had never known about. And I will be looking at the `ts-essentials` package to see if there are any more useful features to help build more robust code.

## Resources

[MancJS | MeetupMancJS is Manchester’s monthly user group for JavaScript programmers and software development enthusiasts. We cover a range of topics, including JavaScript, with a focus on web/mobile development, languages, techniques and tools. We have monthly talks and occasional hands-on sessions.Visit our site![](https://secure.meetupstatic.com/next/images/general/m_swarm_196x196.png)Meetup![](https://secure.meetupstatic.com/photos/event/1/6/e/d/600_507545869.jpeg)](https://www.meetup.com/mancjs/?ref=mrstebo.co.uk)

[The starting point for learning TypeScriptFind TypeScript starter projects: from Angular to React or Node.js and CLIs.![](https://www.typescriptlang.org/icons/icon-512x512.png?v=8944a05a8b601855de116c8a56d3b3ae)](https://www.typescriptlang.org/docs/?ref=mrstebo.co.uk)

[AssemblyScriptA TypeScript-like language for WebAssembly![](https://www.assemblyscript.org/favicons/apple-touch-icon.png)![](https://avatars.githubusercontent.com/u/1136893?v=4&s=64)](https://www.assemblyscript.org/?ref=mrstebo.co.uk)

[GitHub - ts-essentials/ts-essentials: All basic TypeScript types in one place 🤙All basic TypeScript types in one place 🤙. Contribute to ts-essentials/ts-essentials development by creating an account on GitHub.![](https://github.com/fluidicon.png)GitHubts-essentials![](https://opengraph.githubassets.com/8386e116f25fe03e7aaca1d7235034284d0a04728fcd7f397bc52570b3b7a2b9/ts-essentials/ts-essentials)](https://github.com/ts-essentials/ts-essentials?ref=mrstebo.co.uk)