Typescript
Types from both keys and values of object in Typescript
TypeScript provides powerful type system features that allow developers to define precise structures for their data. One advanced technique involves deriving types from both keys and values of objects. This approach enhances type safety and code maintainability by ensuring that your types accurately reflect the shape of your data. Understanding how to extract types from object keys and values is crucial for leveraging the full potential of TypeScript, especially when working with complex data structures or external APIs. Mastering these techniques allows for creating more robust and predictable applications, reducing the likelihood of runtime errors related to type mismatches. This article delves into practical examples and explanations of how to effectively use this feature.
Understanding Keyof and Typeof in TypeScript
TypeScript’s keyof and typeof operators are foundational for extracting types from both keys and values of objects. The typeof operator returns the TypeScript type of a value. This is particularly useful when dealing with existing JavaScript objects or when you need to define a type based on the structure of an object. Using typeof allows you to create a static type that mirrors the structure of a variable, ensuring type consistency throughout your application.
The keyof operator, on the other hand, extracts the keys of an object type as a union of string literal types. This means that if you have an object type with keys ’name’, ‘age’, and ‘city’, keyof will produce the type “name” | “age” | “city”. This is extremely valuable for creating type-safe lookups and manipulations of object properties. By combining keyof with other TypeScript features like generics and mapped types, you can create highly flexible and type-safe code.
For example, consider the following TypeScript code:
typescript const person = { name: “Alice”, age: 30, city: “New York”, }; type Person = typeof person; // Type is { name: string; age: number; city: string; } type PersonKeys = keyof Person; // Type is “name” | “age” | “city” Here, Person is inferred to be the type of the person object, and PersonKeys is a union of string literals representing the keys of the Person type. These operators are the bedrock for more complex type manipulations and are essential for advanced TypeScript development. Using these allows for type safe access to object properties. For more detailed information, refer to the official TypeScript documentation. TypeScript Keyof Types
Extracting Value Types Using Indexed Access Types
Once you have the keys of an object as a type, you can use indexed access types to extract the corresponding value types. Indexed access types use the syntax ObjectType[KeyType] to look up a specific property type within an object type. This is particularly powerful when combined with keyof because it allows you to iterate over the keys of an object type and extract the type of each value.
For instance, continuing from the previous example, you can extract the type of the age property of the Person type using Person[“age”], which would result in the type number. More generally, you can use a type variable that extends keyof Person to dynamically extract value types based on the key. This approach makes it possible to create generic functions and types that can work with objects of varying shapes.
The following example demonstrates how to create a type that represents the type of any value in the Person object:
typescript type PersonValues = Person[keyof Person]; // Type is string | number In this case, PersonValues is a union of all the value types in the Person object, which are string (for name and city) and number (for age). This technique is useful when you need to work with values of an object without knowing their specific types in advance. According to a Stack Overflow survey, TypeScript’s adoption has increased significantly, highlighting its importance. Stack Overflow Developer Survey 2022
Here’s a summary of key benefits:
- Enhanced type safety by ensuring accurate representation of data structures.
- Improved code maintainability through static typing and compile-time error checking.
Advanced Techniques: Mapped Types and Conditional Types
To take types from both keys and values of objects extraction to the next level, TypeScript offers mapped types and conditional types. Mapped types allow you to transform the properties of an existing type into a new type. This is often used to create types with optional properties, read-only properties, or to apply a specific transformation to each property type. Conditional types enable you to define types that depend on a condition, allowing for more dynamic and context-aware type definitions.
For example, you can use a mapped type to create a type where all properties of the Person type are optional:
typescript type PartialPerson = { [K in keyof Person]?: Person[K]; }; // Type is { name?: string; age?: number; city?: string; } Here, the PartialPerson type has the same keys as Person, but each property is optional. Similarly, you can create a type where all properties are read-only:
typescript type ReadonlyPerson = { readonly [K in keyof Person]: Person[K]; }; // Type is { readonly name: string; readonly age: number; readonly city: string; } Conditional types, on the other hand, can be used to define types based on whether a certain condition is met. For instance, you can create a type that returns string if a given type is number, and number otherwise:
typescript type StringOrNumber
To extract the value types from an object where you need to conditionally modify them based on their key, you can combine mapped and conditional types. Here’s how:
typescript type ConditionalValueTypes
The techniques discussed above have numerous real-world applications. One common use case is working with APIs that return JSON data. By using typeof to infer the type of the JSON response and keyof and indexed access types to extract specific value types, you can ensure that your code is type-safe and resilient to changes in the API. Another use case is creating generic functions that operate on objects with different shapes. By using type variables and conditional types, you can define functions that can handle a wide range of object types while still maintaining type safety.
Consider a scenario where you are building a form component that needs to dynamically render input fields based on a configuration object. The configuration object might specify the type of each input field (e.g., text, number, boolean) and other properties such as validation rules. By using mapped types and conditional types, you can create a type that accurately reflects the structure of the form configuration and use it to generate the appropriate input fields. This approach not only ensures type safety but also makes your code more modular and reusable.
Here are some common use cases:
- API response type definitions.
- Dynamic form generation.
- Generic data processing functions.
For example, if you are working with a database that returns objects with varying properties, you can use these techniques to define types that accurately represent the structure of the data. This allows you to write type-safe queries and data transformations, reducing the likelihood of runtime errors. According to research, using TypeScript can reduce bugs by up to 15%. Microsoft Research on TypeScript
FAQ Section
- What is the difference between typeof and keyof?
- typeof returns the type of a value, while keyof returns a union of string literal types representing the keys of an object type.
- How can I extract the type of a specific property in an object?
- Use indexed access types with the syntax ObjectType\["propertyName"\].
- Can I use these techniques with interfaces?
- Yes, keyof and indexed access types work with both types and interfaces.
- What are mapped types used for?
- Mapped types are used to transform the properties of an existing type into a new type.
- Are conditional types necessary?
- Conditional types enable you to define types that depend on a condition, allowing for more dynamic and context-aware type definitions and are helpful in more complex scenarios.
Ready to improve your TypeScript skills? Start experimenting with these techniques in your projects today. By applying these principles, you can create more robust and scalable applications. Consider exploring advanced TypeScript features like generics and utility types to further enhance your understanding. Don’t hesitate to dive deeper into the official TypeScript documentation for comprehensive insights and practical examples.
Question & Answer :
I have two sets of string values that I want to map from one to the other as a constant object. I want to generate two types from that mapping: one for keys and one for values.
const KeyToVal = { MyKey1: 'myValue1', MyKey2: 'myValue2', };
The keys are easy enough:
type Keys = keyof typeof KeyToVal;
I’m having trouble getting a compile-time type for the values. I thought maybe one of these would work:
type Values = typeof KeyToVal[Keys]; type Values<K> = K extends Keys ? (typeof KeyToVal)[K] : never; type Prefix< K extends Keys = Keys, U extends { [name: string]: K } = { [name: string]: K } > = {[V in keyof U]: V}[K];
All of these just made Values to be string. I also tried adapting the two answers to How to infer typed mapValues using lookups in typescript?, but either I got my adaptations wrong, or the answers didn’t fit my scenario in the first place.
The compiler will widen string literal type to string, unless some specific conditions are met as explained in github issues and PR, or const assertion is used for literal value. Const assertions appeared in TypeScript 3.4:
const KeyToVal = { MyKey1: 'myValue1', MyKey2: 'myValue2', } as const; type Keys = keyof typeof KeyToVal; type Values = typeof KeyToVal[Keys]; // "myValue1" | "myValue2"
Prior to 3.4, there was a workaround to get the same effect. To make the compiler infer literal types, you had to pass your object through a function with appropriately crafted generic type parameters, this one seems to do the trick for this case:
function t<V extends string, T extends {[key in string]: V}>(o: T): T {return o}
The whole purpose of this function is to capture and preserve types to enable type inference, it’s entirely useless otherwise, but with it you can have
const KeyToVal = t({ MyKey1: 'myValue1', MyKey2: 'myValue2', }); type Keys = keyof typeof KeyToVal; type Values = typeof KeyToVal[Keys]; // "myValue1" | "myValue2"