As TypeScript applications get more complex so do the types required to describe it. There are a number of built-in types that can be used for this purpose or combined to create more specialised types.

What I term modifying types such as Partial and Required are included in the language and I will quickly cover these first to warm up for the deeper types we’ll address later.

This article will quickly move on to focus on the slightly more advanced types beginning with Extract. You can see the source of the various types by looking at the lib.es5.d.ts declaration file inside TypeScript.

Partial

This generic type takes a single argument, an object type, and returns a new type where all the properties are defined as optional.

interface UserRecord {
  name: string;
  age: number;
}

type User = Partial<UserRecord>;

With the application of the Partial type TypeScript will interpret User as the following type where all properties are now optional.

type User = {
  name?: string;
  age?: number;
};

You can also get a little creative and keep some properties required when applying partial.

type User = Partial<UserRecord> & { name: UserRecord["name"] };

Whilst this works and the name property is now mandatory there are easier ways to do this that will become apparent further into this article.

Required

Much like partial this type takes a single argument of an object type and returns a new type where all the properties are required.

interface ComputerRecord {
  clockSpeed?: number;
  ram?: number;
}
type Computer = Required<ComputerRecord>;

Creates a new type with the following form when interpreted by TypeScript.

type Computer = {
  clockSpeed: number;
  ram: number;
};

Readonly

Pretty much what it says on the tin; this type marks all the properties of an object type as readonly.

interface CarRecord {
  make: string;
  seats?: number;
}
type Car = Readonly<CarRecord>;

Revealing a new TypeScript type that takes the following shape.

type Car = {
  readonly make: string;
  readonly seats?: number;
};

Record

This type is a little different to the three types that we’ve already reviewed so far; it takes two arguments. A union of keys and a type. With this information TypeScript will construct a new type that includes each of these keys set to the supplied type.

type Building = Record<"streetNumber" | "floors" | "bedrooms", number>;

Which TypeScript will expand into the following type when it is interpreted.

type Building = {
  streetNumber: number;
  floors: number;
  bedrooms: number;
};

Again, you can get a little creative with this type and do some things like this.

type Building = Partial<Record<"streetNumber" | "floors" | "bedrooms", number>>;

Will create a type when interpreted that looks a lot like what you might write as:

type Building = {
  streetNumber?: number;
  floors?: number;
  bedrooms?: number;
};

Another neat trick is to use Record to create types that include properties of multiple types.

type Plant = Record<"name" | "family", string> &
  Record<"height" | "age", number>;

That will create a type that will be interpreted into the following:

type Plant = {
  name: string;
  family: string;
  height: number;
  age: number;
};

Extract (better known as intersection)

Set notation: A∩B

Extract venn diagram: includes a and b, but excludes x and z

Items that exist in both the first and second arguments to the type are kept, but unique items from either side are dropped. This type essentially fills the role of an intersection between two types.

type T1 = Extract<"a" | "b" | "x", "a" | "b" | "z">; // 'a' | 'b'

Describing the same operation in TypeScript code this type could be written using the in-built Array.prototype.filter() function.

const t1 = ["a", "b", "x"].filter((x) => ["a", "b", "z"].includes(x)); // ['a', 'b']

If you have a two union types and you want to the find the intersection then Extract is very useful.

Exclude (better known as difference)

Set notation: A – B

Exclude venn diagram: includes x, but excludes a, b and z

Calculates the difference between two types (important to note that this is not the symmetrical difference). Everything that exists in the first argument excluding all items that appear in the second argument will be included in the resultant type.

// keep everything from the left excluding any from the right
type T2 = Exclude<"a" | "b" | "x", "a" | "b" | "z">; // 'x'

This can also be described by the following TypeScript implementation code.

const t2 = = ['a', 'b', 'x'].filter(
  x => !(['a', 'b', 'z'].includes(x))
) // ['x']

Exclude is used to narrow union types back down again. I am including the following code as a demonstration, but it is not production ready code and in some ways takes the form of pseudocode.

enum ConfigType {
  INI,
  JSON,
  TOML,
}

interface ConfigObject {
  name: string;
  port: number;
}
type JSONConfig = string;
type TOMLConfig = string;
type INIConfig = string;
type ENVConfig = ConfigObject;

type Config = JSONConfig | TOMLConfig | INIConfig | ENVConfig;
type UnparsedConfig = Exclude<Config, ENVConfig | ConfigObject>;
type ParsedConfig = Exclude<Config, UnparsedConfig>;

function loadJsonConfig(cfg: JSONConfig): ParsedConfig {
  return JSON.parse(cfg);
}

function loadTomlConfig(cfg: TOMLConfig): ParsedConfig {
  return TOML.parse(cfg);
}

function loadIniConfig(cfg: INIConfig): ParsedConfig {
  return INI.parse(cfg);
}

function loadConfig(cfg: UnparsedConfig): ParsedConfig {
  if (isType(ConfigType.JSON, cfg)) {
    return loadJsonConfig(cfg);
  } else if (isType(ConfigType.TOML, cfg)) {
    return loadTomlConfig(cfg);
  } else if (isType(ConfigType.INI, cfg)) {
    return loadIniConfig(cfg);
  }
}

Pick

Set notation: A∩B

Pick venn diagram: includes a and b, but excludes x

Similar to an intersection, but it is based on the keys defined in the first type argument. The second argument is a list of the keys to copy into the new type.

type T3 = Pick<{ a: string; b: number; x: boolean }, "a" | "b">;
// { a: string, b: number }

Here is a very contrived example of a possible use for Pick:

interface Config {
  host: {
    uri: string;
    port: number;
  };
  authentication: {
    oauth: {
      uri: string;
    };
  };
}

// these get config functions could be loading from the environment
// or different files etc in a real application. Here they are hard
// coded for demonstration purposes.
const getHostConfig = (): Pick<Config, "host"> => ({
  host: {
    uri: "http://example.org",
    port: 1337,
  },
});

const getAuthConfig = (): Pick<Config["authentication"], "oauth"> => ({
  oauth: {
    uri: "http://example.org",
  },
});

const main = (cfg: Config) => {
  // this is where you application code would probably be
};

// assemble the final config object by piecing together
// the various parts that were loaded up from the env etc.
// and start the application
main({ ...getHostConfig(), authentication: getAuthConfig() });

Omit

Set notation: A – B

Omit venn diagram: includes x, but excludes a and b

Again, this type is similar to the Exclude type, but it takes an object type and a list of keys. The keys indicate which properties should be dropped from the new object type.

type T4 = Omit<{ a: string; b: number; x: boolean }, "a" | "b">;
// { x: string }

This has recently been added to the set of types that come with TypeScript by default in 3.5, but older code will need to implement this manually using code like the follow.

Notice how it builds upon two types that we’ve already looked at - Exclude and Pick.

export type Omit<T extends object, K extends keyof T> = Pick<
  T,
  Exclude<keyof T, K>
>;

Using the same example types as Pick we could have a function something like the following:

const startServer = (cfg: Omit<Config, "authentication">) => {
  http.listen(cfg.host.port, () => {
    console.log("Started...");
  });
};

Difference (symmetrical)

Set notation: ‘(A∩B) or (A∪B) - (A∩B)

Difference venn diagram: includes x and z, but excludes a and b

Providing types for symmetrical difference is a little more difficult. This is where values that are unique from both the left and right should be included in the resultant type. Essentially this will lead to a final type that will be used in the following way.

type T5 = Difference<
  { a: number; b: number; x: number },
  { a: number; b: number; z: number }
>; // { x: number; z: number }

As I mentioned this is a fair bit more difficult than it sounds and there are a number of steps required so hang in there.

To produce this we must first workout the difference between the keys in each of the input types. We’ll first write a key differencing type - AMinusB. This will take two object types and keep all the keys of A that do not exist in B.

export type AMinusB<A extends keyof any, B extends keyof any> = ({
  [P in A]: P;
} & { [P in B]: never } & { [x: string]: never })[A];

The set notation for this is A - B (as you would expect) and that makes this type is very similar to one that we’ve just explored - Omit. AMinusB is a little different in that it can take any two objects and calculate the keys that exist in A, but not in B. Omit on the other hand dictates that the keys it is supplied are on the object it is given.

To get the symmetrical difference of the keys we can execute the AMinusB type twice and join them in a sum type.

export type SymmetricalKeyDiff<A extends object, B extends object> =
  | AMinusB<keyof A, keyof B>
  | AMinusB<keyof B, keyof A>;

Note that the key lists are flipped between the two calls to AMinusB so as to get key difference both ways - thus powering the “symmetrical” part of this difference type.

With these two key types we can now create the final differencing type that will take the keys and apply them to an object type. Given what we’ve already learned about the inbuilt types we know that Pick takes an object type and a list of keys and will return a new object type with just the specified properties/keys.

So, given SymmetricalKeyDiff and Pick we can create a symmetrical difference type. The input object for Pick is the union of A and B and the list of keys is the SymmetricalKeyDiff of A and B.

export type SymmetricalDiff<A extends object, B extends object> = Pick<
  A & B,
  SymmetricalKeyDiff<A, B>
>;

Putting this type into action looks something like this:

type T5 = SymmetricalDifference<
  { a: number; b: number; x: number },
  { a: number; b: number; z: string }
>; // { x: number; z: string }

Intersection

Using the same basic underlying types it is also possible to get the intersection of two object types.

export type Intersection<A extends object, B extends object> = Omit<
  A & B,
  SymmetricalKeyDiff<A, B>
>;

Put into practice this type can be used in the following way:

type T6 = Intersection<
  { a: number; b: number; x: number },
  { a: number; b: number; z: string }
>; // { a: number; b:number }

So, there you have it - some reasonably complicated types defined in TypeScript. Hopefully, you’ve been able to follow along until the end and you get some use out of what you’ve learnt here.