Syed Umar AnisJavascriptType transformations in TypeScript: Removing functions from a type
Syed Umar AnisJavascriptType transformations in TypeScript: Removing functions from a type

TypeScript is often quoted as one of the most loved programming languages (StackOverflow, 2022). I guess, what contributes to it is the ease with which TypeScript can be introduced in a JavaScript project and the benefits it bring like static type checking, null safety, better refactoring and IntelliSence support.

Another aspect of TypeScript which I really value is its powerful and expressive Type system. TypeScript allows us to create types based on other types. It is very easy to create a readonly or optional version of a type. For instance, Readonly<Person> gives you a version of Person type where all properties are readonly.

type Employee = {
  name: string;
};
 
const employee: Readonly<Employee> = {
  name: "Syed Umar Anis",
};
 
employee.name = "Tom"; // error
// Cannot assign to 'name' because it is a read-only property.

In today’s post, we are going the create a utility class for removing functions from a type. The technique can be used for removing any kind of properties from a type.

First of all, it is very easy to remove a named property from a class. For instance, following is going to remove salary property from Employee type.

type EmployeeWithoutSalary = Omit<Employee, 'salary'>;

Our objective is to create a generic utility which can remove all functions from a type without knowing the function names. This will involve a bit more work and we will learn a few concepts along the way.

Literal Types

TypeScript variables can have types which are literals.

type Employee = {
  kind: 'employee';
};

Here the kind variable has a type ’employee’. It means that it can have only one possible value i.e. ’employee’. It doesn’t seem useful to have a variable which can have just one specific value but it is a powerful feature when combined with other TypeScript construct like unions.

Union Type

Here, the kind property can have one of the two values: employee or contractor. We have specified that using the union of two literal types.

type Employee = {
  kind: 'employee' | 'contractor';
};

Union types can be used any types you may have:

string | number | Employee

keyof Operator

It creates a new type which is a union of property names or keys of an object.

type Employee = {
  name: string;
  salary: number;
};

type myType = keyof Employee;

myType will be equal to 'name' | 'salary' which is a union of string literal types.

Indexed Access Types

Using indexed access type, we can lookup the type of a property on another type.

type newType1 = Employee['name'];        // newType1 equivalent to string
type newTypes = Employee['name' | 'age'] // newType2 equivalent to string | number

Mapped Types

Here is a basic mapped type:

type Employee = {
  [key: string]: string | number;
};

The above Employee class declares a types which can have any property as long as it is a string and the return type is limited to either number and string.

One way to limit the keys is to base it on the keys of another type.

type Employee = {
  [key in keyof Person]: string | number;
};

In the above Employee type, the object properties are limited to the properties of Person type.

To match the property types of Employee with that of Person, we can use indexed access types:

type Employee = {
  [key in keyof Person]: Employee[key];
};

At this point, it is not very useful as it is giving the exact same type as Person. But it can be useful if we want to define additional fields in Employee type or want the change to type of a property.

Let’s say we want to create a type which has same properties as Employee but the property types should be boolean. Here we go:

type EmployeeWithBoolKeys = {
  [key in keyof Employee]: boolean;
};

Let’s turn this into a generic utility which can be applied to any type and create a new type with boolean properties.

type TypeWithBoolKeys<T> = {
  [key in keyof T]: boolean;
};

type EmployeeWithBoolKeys = TypeWithBoolKeys<Employee>;

Conditional Types

Let’s modify the above generic utility class to conditionally change property types to either boolean or string.

type TypeWithBoolOrStringKeys<T> = {
  [key in keyof T]: T[key] extends boolean? string : boolean;
}

T[key] is the Indexed Access Type we learned about above. It gives us the type of the key property in type T. We check the type of T[key] if it is boolean using extends keyword. If the property type is boolean, it is changed to string, otherwise it is changed to boolean.

never type

never is one of the built-in types in TypeScript. If type of a property is never, it cannot be used.

Removing functions from a type

Now, we have learnt all the concepts involved in creating a generic utility to remove functions from a type. Let’s get on to it.

type RemoveFunctions<T> = {
  [key in keyof T] : T[key] extends Function? never : T[key];
};

The above generic utility will transform a given type T into a type where all keys are the same except functions. The type of functions will be changed to never. Functions will still show up in the type definition but with never type and calling them generates an error from TypeScript type checker.

This should be good enough, but as we are creating a generic utility we want to completely remove functions (we don’t even want them with never type). This will take some work.

First we want to extract the key names which are not function.

type RemoveFunctions<T> = {
  [key in keyof T] : T[key] extends Function? never : key;
};

In the above code, we have changed the type of non-function properties from T[key] to just key. So, the key type now will be a string literal, same as the key name.

Next, we are going to extract the type of all keys:

type NonFunctionKeyNames<T> = {
  [key in keyof T] : T[key] extends Function? never : key;
}[keyof T];

We have added [keyof T] at the end. This is going the extract the type of all keys which is the key names for non-function types (function keys with never type will get excluded). It may also include undefined if you have optional properties in your class.

Now, we are going to exclude undefined from the key names:

type NonFunctionKeyNames<T> = Exclude<{
  [key in keyof T] : T[key] extends Function? never : key;
}[keyof T], undefined>;

We have used the Exclude<UnionType, ExcludedMembers> utility class to remove undefined (NonNullable could also be used).

Now, we have a list of keys which are non-functions. We can use Pick to create a new type which only has non-functions properties.

Finally, here is the final version of a utility class which is going to remove functions from a type.

type NonFunctionKeyNames<T> = Exclude<{
  [key in keyof T] : T[key] extends Function? never : key;
}[keyof T], undefined>;

type RemoveFunctions<T> = Pick<T, NonFunctionKeyNames<T>>;

So, the following is going the give a type which is same as Employee but without functions:

type EmployeeWithoutFunc = RemoveFunctions<Employee>;

Hi, I’m Umar

One Comment

Leave a Reply

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