Reducing typing with Util Types in Typescript

In this publication, we will look at how you can reduce the number of types / interfaces in Typescript, make them more concise and save time for yourself and your team. We will do all this with Utility Types – special types provided by Typescript.

https://monkeyuser.com

https://monkeyuser.com

What are Util Types?

TypeScript provides utilities to facilitate type conversion. These types are available globally, add flexibility to type creation, and help maintain the DRY principle.

Optional fields in types – Partial

Let’s say we want to make all fields in a structure optional:

interface UserData {
  uuid: string;
  username: string;
  age: number;
}

// Реализация такого типа ручками:
interface OptionalUserData {
  uuid?: string;
  username?: string;
  age?: number;
}

transformation as in OptionalUserData can be done with a special type Partial<T>. All Util Types contain a generic to which we pass the type we need to convert.

Partial<T> converts all fields in the type and makes them optional:

interface UserData {
  uuid: string;
  username: string;
  age: number;
}

type OptionalUserData = Optional<UserData>; // Эквивалентно интерфейсу OptionalUserData, что был в листинге до этого

Make all fields required – Required

Required is the opposite Partial – it makes all fields required.

interface UserData {
  uuid: string;
  username: string;
  age?: number; // Опциональное поле
}

type RequiredUserData = Required<UserData>;

// Property 'age' is missing in type '{ age: number; }' but required in type 'RequiredUserData'.
const person: RequiredUserData = {
  uuid: "fff",
  username: "tokiory"
};

Creating Strongly Typed Objects – Record

Record is used to create strongly typed objects.

  • Type T needed to specify the key type for the object;

  • Type U indicates the type of the field’s value.

interface PetInfo {
  age: number;
  breed: string;
}
 
type CatName = "lucas" | "oiko" | "jean";
 
const cats: Record<CatName, CatInfo> = {
  lucas: { age: 1, breed: "Maine Coon" },
  oiko: { age: 15, breed: "Persian" },
  jean: { age: 16, breed: "British Shorthair" },
};

Important

If you specify instead CatName in the example above string, then you will lose Intellisense for this object in the editor. The fact is that Typescript will think that inside an object with type Record<string, ...> you can put any string as a key and it won’t parse the keys you provided inside the object.

Making typed values ​​with autocompletion

If we just create Record<string, ...>then we will not see auto-completion.

Typescript as mentioned above will assume that Record<string, ...> can have any field that falls under the type string:

interface PersonInfo {
  isOnline: boolean;
  age: number;
}

type PersonalInfo = Record<string, PersonInfo>;

const persons: PersonalInfo = {
  john12: {
    isOnline: true,
    age: 20
  },
  tokiory: {
    isOnline: false,
    age: 20
  },
};

// Если мы напишем "persons." - то Intellisense не поймет чего мы от него хотим

Instead of this approach, we should use the operator satisfieswhich was introduced in Typescript 4.9.

interface PersonInfo {
  isOnline: boolean;
  age: number;
}

type PersonalInfo = Record<string, PersonInfo>;

const persons = {
  john12: {
    isOnline: true,
    age: 20
  },
  tokiory: {
    isOnline: false,
    age: 20
  },
} satisfies PersonalInfo;

// Если мы напишем "persons." - то Intellisense заработает!!!

The idea is that the operator satisfies will check each property and value from persons for compatibility with source types in PersonalInfo.

For example, field john12 is a special case of the type string, satisfies sees it
and understands that the key of the object satisfies that all keys must be of type string and doesn’t throw any error.

It is important to note that, unlike the first approach, the second only checks for compatibility with the original types,
object types persons not assigned, that is persons has type:

const persons: {john12: {isOnline: boolean, age: number}, tokiory: {isOnline: boolean, age: number}}

Types for Functions – Parameters and ReturnType

These types are used to extract types from functions.

  • Parameters<T> – returns an array of parameter types in the function;

  • ReturnType<T> – returns the return type of the function.

function capitalize({line}: {line: string}): string {
  return s.length ? s[0] + s.slice(1) : s;
}

type CapitalizeFuncArgs = Parameters<typeof capitalize>; // [ { line: string } ]
type CapitalizeFuncReturn = ReturnType<typeof capitalize>; // string

Working with interface fields – Pick and Omit

Pick And Omit are mainly used with interfaces for convenient work with fields.

  • Pick<T, U> – allows you to return one or more fields from the interface;

  • Omit<T, U> – allows you to return the entire interface without certain fields.

interface Person {
  name: string;
  surname: string;
  age: number;
  login: string;
  email: string;
}

type LoginInfo = Pick<Person, "login" | "email" | "name">; // => { login: string; email: string; name: string; }

type ShowName = Omit<Person, "login" | "email">;           // => { name: string; surname: string; age: number; }

Common Elements and Unique Elements – Extract and Exclude

  • Extract – returns all types that are in one and the other passed union of types;

  • Exclude – returns all types that are either in the first or in another type union passed;

type MaleName = "Carl" | "Christian" | "Daren";
type FemaleName = "July" | "Daren" | "Ann";

type OnlyMaleName = Exclude<MaleName, FemaleName>; // "Carl" | "Christian"
type OnlyGeneralName = Extract<MaleName, FemaleName>; // "Daren"

Interaction with String Types

Below are provided types for interacting with string types:

  • Uppercase<StringType> – A type that converts all characters to uppercase;

  • Lowercase<StringType>– A type that converts all characters to lower case;

  • Capitalize<StringType>– A type that converts the first character to upper case;

  • Uncapitalize<StringType>– A type that converts the first character to lower case.

type Cat = "cAt";
type UpperCat = Uppercase<Cat>; // "CAT"
type LowerCat = Lowercase<Cat>; // "cat"
type CapitalCat = Capitalize<Cat>; // "CAt"
type UncapitalCat = Capitalize<Cat>; // "cAt"

If you pass data an allied type, then Typescript will process the type for each element of the union:

type Pet = "doG" | "CaT";
type UpperCat = Uppercase<Cat>; // "DOG" | "CAT"
type LowerCat = Lowercase<Cat>; // "dog" | "cat"
type CapitalCat = Capitalize<Cat>; // "DoG" | "CaT"
type UncapitalCat = Capitalize<Cat>; // "doG" | "caT"

Instead of a conclusion

If you liked this article, then you can always go to my blogthere is more related information about web development.

If you have any questions – feel free to ask them in the comments. Have a good time! 💁🏻‍♂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *