YouTip LogoYouTip

Ts Indexed Types

Indexed Types and the keyof Keyword in TypeScript |

Indexed types and keyof are powerful tools for manipulating object types in TypeScript.

They allow us to dynamically access object properties and create flexible type mappings.


Why Indexed Types Are Needed

In JavaScript, we often need to dynamically access object properties.

For example, a function might need to get all keys of an object or access the value type based on a key.

Indexed types and keyof enable us to express this dynamism within the type system while maintaining type safety.

Concept: Indexed types are a class of types that allow dynamic access to object properties, and keyof is used to obtain a union type composed of all keys of an object type.


The keyof Operator

The keyof operator is used to get a union type composed of all keys of an object type.

Example

// Define user type

interface User {

 id: number;// User ID

 name: string;// Username

 email: string;// Email

 age?: number;// Age (optional)

}

// Use keyof to get the union type of all keys

// Result: "id" | "name" | "email" | "age"

type UserKeys = keyof User;

// Test keyof

function getProperty<T, K extends keyof T>(obj: T, key: K): T{

    return obj;

}

const user: User ={

 id:1,

 name:"Alice",

 email:"alice@example.com"

};

// Get name property

const userName: string = getProperty(user,"name");

console.log("Username: "+ userName);

Output:

Username: Alice

Note: keyof returns a literal union type of key names, not a string type.


Index Access Types

Using index access types allows you to retrieve the type of an object property.

Example

// Define user type

interface User {

 id: number;

 name: string;

 email: string;

}

// Use index access types to get property types

type UserId = User;// number

type UserName = User;// string

// Can use union types for multiple key access

type UserIdAndName = User["id"|"name"];// number | string

// Use keyof to get a union of all property types

type AllUserValues = User;// number | string

// Practical usage example

function getValue<T, K extends keyof T>(obj: T, key: K): T{

    return obj;

}

const user: User ={ id:1, name:"Bob", email:"bob@test.com"};

// Get id type

const idValue: number = getValue(user,"id");

console.log("ID: "+ idValue);

// Get name type

const nameValue: string = getValue(user,"name");

console.log("Name: "+ nameValue);

Index Access: Using square brackets [] allows accessing specific property types of an object type, which is very powerful and flexible.


Basic Mapped Types

Mapped types allow creating new types based on existing ones by iterating over their keys.

Example

// Define user type

interface User {

 id: number;

 name: string;

 email: string;

 age: number;

}

// Make all properties optional

type PartialUser = Partial;

// Make all properties read-only

type ReadonlyUser = Readonly;

// Custom mapped type: make all properties optional and stringified

type Stringify<T>={

    : string;

};

type StringifiedUser = Stringify;

// Practical usage example

const partialUser: PartialUser ={

 id:1,

 name:"Alice"

// email and age are optional

};

const readonlyUser: ReadonlyUser ={

 id:1,

 name:"Bob",

 email:"bob@test.com",

 age:25

};

// readonlyUser.name = "Charlie"; // Error: read-only

console.log("Partial user: "+ partialUser.name);

console.log("Read-only user: "+ readonlyUser.name);

Mapped Types: The syntax iterates through all keys of a type, forming the basis for creating utility types.


Constraining Key Types

Use keyof with generic constraints to limit the keys accepted by a function.

Example

// Define configuration type

interface Config {

 apiUrl: string;

 timeout: number;

 retry:boolean;

}

// Only allow getting existing keys

function getConfigValue<T, K extends keyof T>(

 config: T,

 key: K

): T{

    return config;

}

// Define configuration

const config: Config ={

 apiUrl:"https://api.example.com",

 timeout:5000,

 retry:true

};

// Correct: key exists

const url: string = getConfigValue(config,"apiUrl");

const timeoutVal: number = getConfigValue(config,"timeout");

// Incorrect: key does not exist (TypeScript will report an error)

// const invalid = getConfigValue(config, "unknown");

console.log("API URL: "+ url);

console.log("Timeout: "+ timeoutVal);

Constraint: K extends keyof T ensures that the passed key must exist on the object, preventing runtime errors.


Extracting Properties of Specific Types

Extract property keys of a specific type from an object type.

Example

// Define mixed type

interface Mixed {

 id: number;

 name: string;

 age: number;

 email: string;

 active:boolean;

}

// Extract all string-type keys

type StringKeys<T>={

    : Textends string ? K : never;

};

// Extract all number-type keys

type NumberKeys<T>={

    : Textends number ? K : never;

};

// Test

type StringProps = StringKeys;// "name" | "email"

type NumberProps = NumberKeys;// "id" | "age"

// Practical application: get values of string properties

function getStringProps<T, K extends StringKeys<T>>(obj: T, keys: K[]): T[]{

    return keys.map(key => obj);

}

const mixed: Mixed ={

 id:1,

 name:"Alice",

 age:25,

 email:"alice@test.com",

 active:true

};

const strings = getStringProps(mixed,["name","email"]);

console.log("String properties: "+ strings.join(", "));

Conditional Types: Combining conditional types with mapped types enables complex type filtering and extraction.


Iterating Over Array Types

Use indexed types to operate on arrays and tuples.

Example

// Define tuple type

type Tuple =[string, number,boolean];

// Get element types of tuple

type First = Tuple;// string

type Second = Tuple;// number

type Third = Tuple;// boolean

// Use number to get all element types

type AllElements = Tuple;// string | number | boolean

// Get array element types

type StringArray = string[];

type ArrayElement = StringArray;// string

// Practical application: function overloading

function getElement<T extends any[]>(arr: T, index: number): T|undefined{

    return index < arr.length? arr:undefined;

}

const tuple: Tuple =["hello",123,true];

const arr: string[]=["a","b","c"];

console.log("Tuple element : "+ getElement(tuple,0));

console.log("Array element : "+ getElement(arr,1));

Tuple Indexing: Tuples can be indexed using numeric indices, where each index corresponds to a specific element type β€” something arrays cannot do.


Notes

  • keyof returns a union type: keyof returns a literal union type of key names
  • Safe index access: Ensure accessed keys exist in the target type
  • Generic constraints: Use extends keyof to constrain generic parameters
  • Mapped types require the in keyword: Mapped types use the syntax

Best Practice: Indexed types are the foundation for creating generic utility types. Mastering them significantly enhances your ability to work with types.


Summary

Indexed types and keyof are important components of TypeScript's type system.

  • keyof: Gets all keys of an object type
  • Index Access: Retrieves property types via keys
  • Mapped Types: Creates new types based on existing ones
  • Type Safety: Ensures type safety during dynamic property access

Suggestion: When working with object properties, prefer indexed types to maintain type safety.

← Ts CovarianceTs Unit Testing β†’