Notes on TypeScript: Phantom Types
Tags: typescript, types
Published: 2019-02-03 15:33:44

---
title: Notes on TypeScript: Phantom Types
published: true
description: Better understanding phantom types in TypeScript
tags: typescript, types
series: Notes on TypeScript
---

### Introduction

These notes should help in better understanding `TypeScript` and might be helpful when needing to lookup up how leverage TypeScript in a specific situation. All examples are based on TypeScript 3.2.

### Phantom Types

In the following "Notes on TypeScript" we will talk about `Phantom Types`.
To better understand `Phantom Types`, we will build examples a long the way, that should display where using them can be useful.

*"A phantom type is a parametrised type whose parameters do not all appear on the right-hand side of its definition..."*
[Haskell Wiki, PhantomType](https://wiki.haskell.org/Phantom_type)

Taking a look at the above definition from the Haskell wiki, we understand that phantom types are parametrised types where not all parameters appear on the right-hand side. Let's try to see if we can implement a similar example in TypeScript.

```js
type FormData<A> = string;
```

`FormData` is a phantom type as the `A` parameter only appears on the left side.

Next, we want to enable a library user to create a `FormData` type. What we also want is to restrict the type in certain parts of the library. For example we want to be able to differentiate between *Validated* und *Unvalidated* form data. So let's add two type definitions `Validated` and `Unvalidated`.

```js
type Unvalidated = {_type: "Unvalidated"};
type Validated = {_type: "Validated"};
```

Next, let's implement a `FormData` type that makes it impossible for consumers of the form library to override the `value` type definition. In this specific case we will define `value` with a `never` type.

```js
type FormData<T, D = never> = {value: never} & T;
```

Now we would want to expose a function that receives a string and returns an unvalidated `FormData` type.

```js
type makeFormData = (a: string) => FormData<Unvalidated>;
```

And maybe we want to add an `upperCase` function that does exactly that, take an unvalidated `FormData` and returns an unvalidated `FormData`.


```js
type upperCase = (a: FormData<Unvalidated>) => FormData<Unvalidated>;
```

Let's also add a `validate` definition that either returns the validated input or null.

```js
type Validate = (a: FormData<Unvalidated>) => FormData<Validated> | null;
```

Finally let's add a function that processes our validated data:

```js
type Process = (a: FormData<Validated>) => FormData<Validated>;
```

Now that we have defined our form helper functions, let's see how we can implement these definitions to ensure that the actual form value is always hidden from developer land.

```js
export const makeFormData: MakeFormData = value => {
  return { value } as FormData<Unvalidated>;
};
```

If we recall our `makeFormData` function accepts a string and returns a `FormData<Unvalidated>`, it's important to note, that this should be the only way to create this type. Developer land is prevented from defining the `value` because it's defined as a `never` type.
Once this type has been created, developers can use the returned value to validate or uppercase the value.

Let's see how we can implement the `upperCase` and `validate` functions.

```js
export const upperCase: UpperCase = data => {
  const internalData = data as InternalUnvalidated;
  return { value: internalData.value.toUpperCase() } as FormData<Unvalidated>;
};

export const validate: Validate = data => {
  const internalData = data as InternalUnvalidated;
  if (internalData.value.length > 3) {
    return { value: internalData.value } as FormData<Validated>;
  }
  return null;
};
```

Looking at the above two functions, there is one important aspect we can observe. We need to internally cast the provided input. But what is `InternalUnvalidated`?

```js
type InternalUnvalidated = Unvalidated & {
  value: string;
};

type InternalValidated = Validated & {
  value: string;
};
```

What we are doing here, is defining an internal representation of our data, that is hidden away from developers using the library. We are stating that `value` is a _string_ in this case.
`process` can be written in the same way as the above functions, only that we would be casting to a `InternalValidated` type, as we're expecting a `FormData<Validated>` type.

```js
export const process : Process = (data: FormData<Validated>) => {
  const internalData = data as InternalValidated;
  // do some processing...
  return data; // cast to FormData<Validated>
}
```

We have implemented phantom types and can verify that our library works as expected:

```js
import { makeFormData, validate, upperCase, process } from './phantomTypes'

const initialData = makeFormData("test");
const validatedData = validate(initialData);

// validate("hello") // Type '"hello"' is not assignable to type '{value: never}'
// validate({value: "hello"}) // Type 'string' is not assignable to type 'never'

if (validatedData !== null) {
  // validate(validatedData); // Error! Type '"Validated"' is not assignable to Type '"Unvalidated"'
  upperCase(initialData);
  // upperCase(validatedData) // Error! Type '"Validated"' is not assignable to Type '"Unvalidated"'
  process(validatedData);
  // process(initialData); // Error! Type '"Unvalidated"' is not assignable to Type '"Validated"'
}
```

We could also create our own `PhantomType` type and abstract having to manually take care of handling `_type`:

```js
type PhantomType<Type, Data> = {_type: Type} & Data;

// use this type helper to create an UnvalidatedData type
type UnvalidatedData = PhantomType<"Unvalidated", {value: string}>
```

At this point, we should have a basic understanding of how _phantom types_ can be implemented in TypeScript now. Checkout the full example [here](https://www.typescriptlang.org/play/index.html#src=type%20FormData_%3CT%2C%20D%20%3D%20never%3E%20%3D%20%7B%20value%3A%20never%20%7D%20%26%20T%3B%0D%0A%0D%0Atype%20Unvalidated%20%3D%20%7B_type%3A%20%22Unvalidated%22%7D%3B%0D%0Atype%20Validated%20%3D%20%7B%20_type%3A%20%22Validated%22%20%7D%3B%0D%0A%0D%0Atype%20MakeFormData%20%3D%20(a%3A%20string)%20%3D%3E%20FormData_%3CUnvalidated%3E%3B%0D%0Atype%20UpperCase%20%3D%20(a%3A%20FormData_%3CUnvalidated%3E)%20%3D%3E%20FormData_%3CUnvalidated%3E%3B%0D%0Atype%20Validate%20%3D%20(a%3A%20FormData_%3CUnvalidated%3E)%20%3D%3E%20FormData_%3CValidated%3E%20%7C%C2%A0null%3B%0D%0Atype%20Process%20%3D%20(a%3A%20FormData_%3CValidated%3E)%20%3D%3E%20FormData_%3CValidated%3E%3B%0D%0A%0D%0Atype%20InternalUnvalidated%20%3D%20Unvalidated%20%26%20%7B%0D%0A%20%20value%3A%20string%3B%0D%0A%7D%3B%0D%0A%0D%0Atype%20InternalValidated%20%3D%20Validated%20%26%20%7B%0D%0A%20%20value%3A%20string%3B%0D%0A%7D%3B%0D%0A%0D%0Aconst%20makeFormData%3A%20MakeFormData%20%3D%20value%20%3D%3E%20%7B%0D%0A%20%20return%20%7B%20value%20%7D%20as%20FormData_%3CUnvalidated%3E%3B%0D%0A%7D%3B%0D%0A%0D%0Aconst%20upperCase%3A%20UpperCase%20%3D%20data%20%3D%3E%20%7B%0D%0A%20%20const%20internalData%20%3D%20data%20as%20InternalUnvalidated%3B%0D%0A%20%20return%20%7B%20value%3A%20internalData.value.toUpperCase()%20%7D%20as%20FormData_%3CUnvalidated%3E%3B%0D%0A%7D%3B%0D%0A%0D%0Aconst%20validate%3A%20Validate%20%3D%20data%20%3D%3E%20%7B%0D%0A%20%20const%20internalData%20%3D%20data%20as%20InternalUnvalidated%3B%0D%0A%20%20if%20(internalData.value.length%20%3E%203)%20%7B%0D%0A%20%20%20%20return%20%7B%20value%3A%20internalData.value%20%7D%20as%20FormData_%3CValidated%3E%3B%0D%0A%20%20%7D%0D%0A%20%20return%20null%3B%0D%0A%7D%3B%0D%0A%0D%0Aconst%20process%20%3A%20Process%20%3D%20(data%3A%20FormData_%3CValidated%3E)%20%3D%3E%20%7B%0D%0A%20%20const%20internalData%20%3D%20data%20as%20InternalValidated%3B%0D%0A%20%20internalData.value.toLowerCase()%3B%0D%0A%20%20%2F%2F%20do%20some%20processing...%0D%0A%20%20return%20data%3B%0D%0A%7D%0D%0A%0D%0Aconst%20initialData%20%3D%20makeFormData(%22test%22)%3B%0D%0Aconst%20validatedData%20%3D%20validate(initialData)%3B%0D%0A%0D%0A%0D%0A%2F%2F%20validate(%22hello%22)%20%2F%2F%20Type%20'%22hello%22'%20is%20not%20assignable%20to%20type%20'%7Bvalue%3A%20never%7D'%0D%0A%2F%2F%20validate(%7Bvalue%3A%20%22hello%22%7D)%20%2F%2F%20Type%20'string'%20is%20not%20assignable%20to%20type%20'never'%0D%0A%0D%0Aconst%20upperCasedValue%20%3D%20upperCase(initialData)%3B%0D%0A%0D%0A%20%20%2F%2F%20process(initialData)%3B%20%2F%2F%20Error!%20Type%20'%22Unvalidated%22'%20is%20not%20assignable%20to%20Type%20'%22Validated%22'%0D%0A%0D%0Aif%20(validatedData%20!%3D%3D%20null)%20%7B%0D%0A%20%20%2F%2F%20validate(validatedData)%3B%20%2F%2F%20Error!%20Type%20'%22Validated%22'%20is%20not%20assignable%20to%20Type%20'%22Unvalidated%22'%20%20%0D%0A%20%20%2F%2F%20upperCase(validatedData)%20%2F%2F%20Error!%20Type%20'%22Validated%22'%20is%20not%20assignable%20to%20Type%20'%22Unvalidated%22'%0D%0A%20%20process(validatedData)%3B%0D%0A%7D%0D%0A).

If you have any questions or feedback please leave a comment here or connect via Twitter: [A. Sharif](https://twitter.com/sharifsbeat)