# Going deep on generic types: constraints and defaults

In our last lesson, we introduced the concept of a generic type. But you might have noticed something interesting: in our `Maybe` type, you can pass _any_ type to it:

```ts twoslash
type Maybe<T> = T | null | undefined;

type MaybeString = Maybe<string>;
type MaybeNumber = Maybe<number>;
type MaybeBoolean = Maybe<boolean>;
```

What if you wanted to only allow certain types to be passed?

## Type Parameter Constraints

Let's set up a new example. Error handling in JavaScript can be a bit of a pain. Throwing errors inside a `try`/`catch` block can lead to your errors being typed as `unknown`:

```ts twoslash
// @errors: 18046
const mightFail = () => {
  if (Math.random() > 0.5) {
    throw new Error("Failed!");
  }

  return "Success!";
};

try {
  const result = mightFail();
} catch (error) {
  // error is typed as unknown
  console.log(error.message);
}
```

A different way of thinking about errors is to use a `Result` type, which can either return a success or an error:

```ts twoslash
type Result<T, TError> =
  | { success: true; value: T }
  | { success: false; error: TError };
```

We've got _two_ type parameters here (yes, generic types can have more than one type parameter). The first is the type of the successful value, and the second is the type of the error.

To use this on our `mightFail` function, we can pass in the types we expect:

```ts twoslash
type Result<T, TError> =
  | { success: true; value: T }
  | { success: false; error: TError };

// ---cut---
const mightFail = (): Result<string, Error> => {
  if (Math.random() > 0.5) {
    return { success: false, error: new Error("Failed!") };
  }

  return { success: true, value: "Success!" };
};

const result = mightFail();

if (result.success) {
  console.log(result.value);
} else {
  // result.error is strongly typed!
  console.log(result.error.message);
}
```

This is a great way to handle errors in a more type-safe way. But there's a funny problem - we can pass _anything_ into `TError`:

```ts twoslash
type Result<T, TError> =
  | { success: true; value: T }
  | { success: false; error: TError };

// ---cut---
type Example1 = Result<string, string>;
type Example2 = Result<string, number>;
type Example3 = Result<string, { message: string }>;
```

We want our `TError` to be a bit stricter. We want to make sure that any error thrown is an object with a `message` property, so we know what went wrong.

We can do this by adding a constraint to our `TError` type:

```ts twoslash
type Result<T, TError extends { message: string }> =
  | { success: true; value: T }
  | { success: false; error: TError };
```

The `extends` keyword after `TError` is the _constraint_. It makes sure that any type passed in must have a `message` property.

Now, if we try to pass in a type that doesn't have a `message` property, we'll get an error:

```ts twoslash
// @errors: 2344
type Result<T, TError extends { message: string }> =
  | { success: true; value: T }
  | { success: false; error: TError };

// ---cut---
type Example1 = Result<string, string>;
```

## Constraints Let You Pass Excess Properties

So far, we've been using the metaphor of 'functions' to describe generic types. Our `extends` constraint is kind of like a typed function parameter: restricting what can be passed to the type.

But there's a key difference between constraints and function parameters: constraints let you pass excess properties in the argument.

In our `Result` type, we can pass in any type that has a `message` property. But we can also pass in types that have _more_ properties:

```ts twoslash
type Result<T, TError extends { message: string }> =
  | { success: true; value: T }
  | { success: false; error: TError };

// ---cut---
type Example1 = Result<string, { message: string }>;
type Example2 = Result<
  string,
  { message: string; code: number }
>;
```

In a function, we'd get an error in this situation:

```ts twoslash
// @errors: 2353
function example(param: { message: string }) {
  console.log(param.message);
}

example({ message: "Hello", code: 42 });
```

That's because constraints act as a _minimal specification_ for the type passed in. As long as it's assignable to the constraint, it's allowed.

For instance, we could pass in a type that has a _specific_ `message` string:

```ts twoslash
type Result<T, TError extends { message: string }> =
  | { success: true; value: T }
  | { success: false; error: TError };

// ---cut---
type Example3 = Result<string, { message: "Error" }>;
```

But not a type that has a `message` property that's _not_ a string:

```ts twoslash
// @errors: 2344
type Result<T, TError extends { message: string }> =
  | { success: true; value: T }
  | { success: false; error: TError };

// ---cut---
type Example4 = Result<string, { message: number }>;
```

This means that our `Result` type will output exactly the type passed in, even if it's constrained:

```ts twoslash
type Result<T, TError extends { message: string }> =
  | { success: true; value: T }
  | { success: false; error: TError };

// ---cut---
type Example5 = Result<
  string,
  { message: string; code: number }
>;

type Show = Example5;
//          ^?
```

Note that I've slightly simplified how functions work in my explanation above. To deepen your understanding of the excess properties error, check out the section in [my book](https://github.com/total-typescript/total-typescript-book/blob/main/book-content/chapters/12-the-weird-parts.md#excess-property-warnings).

## Default Type Parameters

We know that in runtime functions, you can add default values to parameters:

```ts twoslash
function add(a: number, b = 5) {
  return a + b;
}

add(5); // 10
```

The same is true in generic types! Let's imagine that when we didn't pass in a `TError` type, we wanted to default to `Error`. We can do this with a default type parameter:

```ts twoslash
type Result<
  T,
  TError extends { message: string } = Error,
> =
  | { success: true; value: T }
  | { success: false; error: TError };
```

Now, if we don't pass in a `TError` type, it will default to `Error`:

```ts twoslash
type Result<
  T,
  TError extends { message: string } = Error,
> =
  | { success: true; value: T }
  | { success: false; error: TError };

// ---cut---
type Example1 = Result<string>;
// { success: true; value: string } | { success: false; error: Error }
```

This is a great way to make your generic types more flexible, and to reduce the amount of boilerplate you have to write.

## Summary

- You can constrain type parameters in generic types with the `extends` keyword.
- Constraints act as a minimal specification for the type passed in, allowing excess properties.
- You can set default type parameters in generic types, just like you can with function parameters.

In the next lesson, we'll start our examination of generic functions by seeing how generic functions really are _everywhere_ - even if we're not writing them ourselves.

Want to get the next lesson now? Click the link below.
