Manipulate Types in TypeScript | Saksham Khandelwal

Post

editor-img
Saksham Khandelwal
Jan 28, 2023

Manipulate Types in TypeScript

TypeScript is a really great language, it has a very powerful type system.

We can combine various type operators to form complex operations and values in a maintainable way.

Keyof Type Operator

We have read about union types, the keyof operator can also be considered a union type. It takes an object type and produces a string or numeric literal union of its keys.

type Point = { x: number; y: number };type P = keyof Point;// type p = keyof Point

If the type has a string or number index signature, keyof will return those types instead:

type Arrayish = { [n: number]: unknown };type A = keyof Arrayish; // type A = number type Mapish = { [k: string]: boolean };type M = keyof Mapish; // type M = string | number

Note: In the 2nd example, M is string | number — this is because JavaScript object keys are always coerced as a string which means obj[0] is always the same as obj["0"].

Typeof Type Operator

The typeof operator is already famous in JavaScript which can be used like this:

// Prints "string"console.log(typeof "Hello world");

TypeScript adds a typeof operator which can be used like this:

let s = "hello";let n: typeof s; // let n: string

Now typeof seems to be a straightforward operator but believe me, if you know how to use it properly, you might use it conveniently to express many patterns.

For example, let’s start by looking at the predefined type ReturnType<T> . It takes the function type and produces its return type.

function f() { return { x: 10, y: 3 };}type P = ReturnType<typeof f>;/* type P = { x: number; y: number;} */

Indexed Access Types

We can use an indexed access type to look up a specific property on another type:

type Person = { name: string; age: number};type Age = Person["age"]; // type Age = number

Since the indexing type is itself a type, so we can use unions, keyof, or other types also:

type I1 = Person["age" | "name"]; type I1 = string | number type I2 = Person[keyof Person]; type I2 = string | number | boolean

We can not use const variable in Index Access Types.

const nameKey= "name";type firstName= Person[nameKey];// Type 'key' cannot be used as an index type.// 'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'?

Conditional Types

I personally think Conditional Types are really useful. These are very similar to ternary operator. These can help to decide a lot of input decision.

type paramType<T> = T extends number ? number : string;function numberOrString<paramType>(param: paramType): void { console.log('numbers, string', param);}numberOrString(1);numberOrString("Vikas");

The type passed in function numberOrString is number then paramType will number else string.

We can then use that conditional type to simplify our overloads down to a single function with no overloads.

let a = createLabel("typescript"); let b = createLabel(2.8);function createLabel<T extends number | string>(idOrName: T): NameOrId<T> { throw "unimplemented";}

We have implemented one function instead of two functions using conditional types.

The power of conditional types comes from using them with generics which is shown in the above all examples.

Mapped Types

Mapped Types are really great for reusing the other types, sometimes you need a type based on another type.

type optionsFlags<Type> = { [Property in keyof Type]: boolean;};type featureFlags = { darkMode: () => void; newUserProfile: () => void;}; type featureOptions = optionsFlags<featureFlags>;

Above example is a very simple perfect example, we can see optionsFlags will take all the properties from featureFlags type and convert their values to be boolean .

Mapped Types are really powerful, we can do lot of great type manipulation.

Below example checks whether the object property name consists firstname , if it contains then return true else false .

type extractFirstName<Type> = { [Property in keyof Type]: Type[Property] extends { firstName: true } ? true : false;}; type DBFields = { id: { format: "incrementing" }; name: { type: string; firstName: true };}; type objectsNeedSurName = extractFirstName<DBFields>;

Template Literal Types

Template literal types in TypeScript allow you to create a new type based on a string template, build on string literal types, these types have the ability to expand into many strings via unions.

type EmailLocaleIDs = "welcome_email" | "email_heading";type FooterLocaleIDs = "footer_title" | "footer_sendoff"; type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`; // type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

Conclusion

By mastering the use of Type Manipulation in TypeScript, you can take your code to the next level of reusability and maintainability.