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:
-
MappedRecord<T, Mapper>: This type should take aRecord<K, V>typeTand aMappertype. TheMappertype should be a conditional type that, for each keyKinT, determines the new key based onK. The value type should remain the same asV. -
MappedRecordValues<T, ValueMapper>: Similar toMappedRecord, but this type should take aRecord<K, V>typeTand aValueMappertype. For each keyKinT, the key should remain the same, but the value typeVshould be transformed byValueMapper. -
PartialRecord<T>: This type should take aRecord<K, V>typeTand produce a new record where all properties are optional. This is similar to TypeScript's built-inPartial, but specifically forRecordtypes. -
ReadonlyRecord<T>: This type should take aRecord<K, V>typeTand produce a new record where all properties are readonly. This is similar to TypeScript's built-inReadonly, but specifically forRecordtypes.
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 whereKis a union of string literal types ornumberliteral types, andVis any valid type. MappedRecordshould expectMapperto be a generic type that takes the key typeKas an argument.MappedRecordValuesshould expectValueMapperto be a generic type that takes the value typeVas an argument.
Notes
- Consider how to iterate over the keys and values of a
Recordtype. - Think about how to apply transformations to keys and values within the mapped type.
PartialRecordandReadonlyRecordare essentially reimplementations of built-in utility types forRecordtypes. While you could technically use the built-ins, the goal here is to understand how to construct them for a specificRecordcontext.- For
MappedRecord, you will need to infer the key typeKand the value typeVfrom the inputRecordtypeT. You can do this by iterating throughkeyof T. - For
MappedRecordValues, you will also inferKandVfromT.