Hone logo
Hone
Problems

TypeScript Record Type Transformation Helpers

In TypeScript, the Record<K, T> utility type is incredibly useful for creating object types with specific keys and value types. However, sometimes you need to transform these records in more complex ways, like mapping keys or values, or creating partial or read-only versions. This challenge focuses on building utility types that help you manipulate Record types effectively.

Problem Description

Your task is to create several TypeScript utility types that operate on Record<K, T> types. These helpers should allow for flexible manipulation of keys and values, mirroring common patterns seen in JavaScript/TypeScript development.

You need to implement the following utility types:

  1. MappedRecord<T, Mapper>: This type should take a Record<K, V> type T and a Mapper type. The Mapper type should be a conditional type that, for each key K in T, determines the new key based on K. The value type should remain the same as V.

  2. MappedRecordValues<T, ValueMapper>: Similar to MappedRecord, but this type should take a Record<K, V> type T and a ValueMapper type. For each key K in T, the key should remain the same, but the value type V should be transformed by ValueMapper.

  3. PartialRecord<T>: This type should take a Record<K, V> type T and produce a new record where all properties are optional. This is similar to TypeScript's built-in Partial, but specifically for Record types.

  4. ReadonlyRecord<T>: This type should take a Record<K, V> type T and produce a new record where all properties are readonly. This is similar to TypeScript's built-in Readonly, but specifically for Record types.

Examples

Example 1: MappedRecord

Let's assume we have a record representing user data:

type UserRecord = Record<string, { name: string; age: number }>;

// We want to transform the keys to be uppercase
type UppercaseKeyMapper<K extends string> = Uppercase<K>;

type UppercaseUserRecord = MappedRecord<UserRecord, UppercaseKeyMapper>;
// Expected type:
// {
//   [K in Uppercase<keyof UserRecord>]: { name: string; age: number };
// }
// (Where keyof UserRecord would be whatever string keys are implicitly available)

// Let's be more specific for demonstration:
type SpecificUserRecord = {
    john: { name: string; age: number };
    jane: { name: string; age: number };
};

type PrefixedKeyMapper<K extends string> = `user_${K}`;

type PrefixedSpecificUserRecord = MappedRecord<SpecificUserRecord, PrefixedKeyMapper>;
// Expected type:
// {
//   user_john: { name: string; age: number };
//   user_jane: { name: string; age: number };
// }

Example 2: MappedRecordValues

Using the SpecificUserRecord from Example 1:

type SpecificUserRecord = {
    john: { name: string; age: number };
    jane: { name: string; age: number };
};

// We want to transform the value type to only contain the name
type ExtractNameMapper<V> = V extends { name: infer N } ? N : never;

type UserNameRecord = MappedRecordValues<SpecificUserRecord, ExtractNameMapper>;
// Expected type:
// {
//   john: string;
//   jane: string;
// }

// Another value transformation: make the age optional
type MakeAgeOptionalMapper<V> = V extends { name: string; age: number } ? { name: string; age?: number } : V;

type UserWithOptionalAgeRecord = MappedRecordValues<SpecificUserRecord, MakeAgeOptionalMapper>;
// Expected type:
// {
//   john: { name: string; age?: number };
//   jane: { name: string; age?: number };
// }

Example 3: PartialRecord and ReadonlyRecord

Using SpecificUserRecord:

type SpecificUserRecord = {
    john: { name: string; age: number };
    jane: { name: string; age: number };
};

type PartialSpecificUserRecord = PartialRecord<SpecificUserRecord>;
// Expected type:
// {
//   john?: { name: string; age: number };
//   jane?: { name: string; age: number };
// }

type ReadonlySpecificUserRecord = ReadonlyRecord<SpecificUserRecord>;
// Expected type:
// {
//   readonly john: { name: string; age: number };
//   readonly jane: { name: string; age: number };
// }

Constraints

  • The utility types must be implemented using TypeScript's conditional types and mapped types.
  • The solution should be purely in TypeScript's type system; no runtime JavaScript code is required.
  • The types should correctly handle any valid Record<K, V> type where K is a union of string literal types or number literal types, and V is any valid type.
  • MappedRecord should expect Mapper to be a generic type that takes the key type K as an argument.
  • MappedRecordValues should expect ValueMapper to be a generic type that takes the value type V as an argument.

Notes

  • Consider how to iterate over the keys and values of a Record type.
  • Think about how to apply transformations to keys and values within the mapped type.
  • PartialRecord and ReadonlyRecord are essentially reimplementations of built-in utility types for Record types. While you could technically use the built-ins, the goal here is to understand how to construct them for a specific Record context.
  • For MappedRecord, you will need to infer the key type K and the value type V from the input Record type T. You can do this by iterating through keyof T.
  • For MappedRecordValues, you will also infer K and V from T.
Loading editor...
typescript