Typescript: Transforming generic types into concrete types

I am utilizing a Generic type

type GenericType = {
  [key: string]: {
    prop1: string,
    prop2?: string,
    prop3?: number,
  },
};

The purpose of the Generic type is to assist in constructing / validating a new object that I have created.

const NewObject: GenericType = {
  key1: {
    prop1: "hi",
  },
  key2: {
    prop1: "bye",
    prop2: "sup",
  },
};

The implementation works as expected. However, when attempting to use the new object, VSCode / TypeScript does not display the keys or properties of NewObject unless I remove GenericType from it. Alternatively, I could "extend" the type but that would lead to redundant code.

NewObject.???

Is there a way to retain the functionality of GenericType while also accessing the specific properties of the new object derived from it?


Update 1

I anticipate that VSCode / TypeScript will show / validate

NewObject.key1.prop1
NewObject.key2.prop1

and generate an error for

NewObject.key1.prop2
NewObject.key2.prop3
NewObject.key2.prop321

Answer №1

The concept behind this is to allow GenericTypes to have any string as a key while still enforcing specific value types for those keys at the declaration point.

To achieve this, the Record type can be used to limit the allowed keys of Obj1 to only the specified ones.

type GenericType<K extends string> = Record<K, {
  prop1: string,
  prop2?: string,
  prop3?: number,
}>

When defining Obj1, you can define the allowed keys by setting a union of keys as the first type parameter.

const Obj1: GenericType<"key1" | "key2"> = {
  key1: {
    prop1: "hi",
  },
  key2: {
    prop1: "bye",
    prop2: "sup",
  },
};

This approach allows TypeScript to provide full type safety when accessing both key1 and key2.

Obj1.key1
// (property) key1: {
//     prop1: string;
//     prop2?: string | undefined;
//     prop3?: number | undefined;
// }

EDIT

Following the OP's preference, rather than specifying all key names or checking optional fields manually, here is an alternative method that ensures the declared object conforms to the constraints of the GenericType interface.

Firstly, a utility type is needed:

type Constraint<T> = T extends Record<string, {
  prop1: string,
  prop2?: string,
  prop3?: number,
}> ? T : never

It will return `never` if `T` does not meet the constraint, otherwise it will return `T` itself.

Next, declare the plain object without type annotations:

const CorrectObj = {
  key1: {
    prop1: "hi",
  },
  key2: {
    prop1: "bye",
    prop2: "sup",
  },
};

Then assign this object literal to another variable, ensuring the new variable is of type

Constraint<typeof CorrectObj>

const CheckedObj: Constraint<typeof CorrectObj> = CorrectObj

If `CorrectObj` fits the constraint, `CheckedObj` will just be a copy with all fields accessible. However, if the literal does not match the constraints, assigning `CheckedBadObj` to it will result in a type error:

const BadObj = {
  key1: {
    progfdgp1: "hi",
  },
  key2: {
    prop1: "bye",
    prdfgop2: "sup",
  },
};

const CheckedBadObj: Constraint<typeof BadObj> = BadObj
//    ^^^^^^^^^^^^^
// Type '{ key1: { progfdgp1: string; }; key2: { prop1: string; prdfgop2: string; }; }' is not assignable to type 'never'. (2322)

The reason being that when `Constraint<T>` fails, it returns `never`, causing a conflict when trying to assign a non-never value to `CheckedBadObj`.

Although there is some redundancy in declaring two instances of each object literal, this method is necessary for having precise knowledge of the object's fields, including nested objects, while verifying their values against set constraints.

Feel free to experiment with this technique in the playground.

Answer №2

If you want key1 and key2 to appear in the autocomplete menu for Obj1, while still being able to add more keys later, consider this solution:

type GenericType = {
  [key: string]: {
    prop1: string,
    prop2?: string,
    prop3?: number,
  };
};

const generify = <T extends GenericType>(obj: T): T & GenericType => obj;

const Obj1 = generify({
  key1: {
    prop1: "hi",
  },
  key2: {
    prop1: "bye",
    prop2: "sup",
  },
});

Currently, I don't have a simpler solution that would provide Obj1 with the same intersection type involving GenericType and a specific type containing only key1 and key2 properties.

Answer №3

Implement a versatile utility function named enforce that:

  • checks if the object adheres to the structure of GenericType using a generic constraint (extends)
  • and infers the type of the passed object.

Sample Code:

type GenericType = {
  [key: string]: {
    prop1: string,
    prop2?: string,
    prop3?: number,
  };
};

const enforce = <T extends GenericType>(obj: T): T => obj;

Demonstration:

type GenericType = {
  [key: string]: {
    prop1: string,
    prop2?: string,
    prop3?: number,
  };
};

const enforce = <T extends GenericType>(obj: T): T => obj;

const SampleObj1 = enforce({
  key1: {
    prop1: "hello",
  },
  key2: {
    prop1: "goodbye",
    prop2: "see you",
  },
});

SampleObj1.key1.prop1; // Accepted
SampleObj1.key2.prop1; // Accepted

/** 
 * ERROR: Properties do not match the input object
 */
SampleObj1.key1.prop2 // Error 
SampleObj1.key2.prop3 // Error
SampleObj1.key2.prop321 // Error

SampleObj1.key3; // Error 

/**
 * ERRORS: Structure does not align with GenericType
 */
const SampleObj2 = enforce({
  key1: { // Error 
  }
});
const SampleObj3 = enforce({
  key1: {
    prop1: 123, // Error
  }
});

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

A tutorial on ensuring Angular loads data prior to attempting to load a module

Just starting my Angular journey... Here's some code snippet: ngOnInit(): void { this.getProduct(); } getProduct(): void { const id = +this.route.snapshot.paramMap.get('id'); this.product = this.products.getProduct(id); ...

TypeScript operates under the assumption that every key will be present on a Record object

Check out this code snippet: declare const foo: Record<string, number> const x = foo['some-key'] TypeScript indicates that x is of type number. It would be more accurate to say x is of type number | undefined, as there is no guarantee th ...

The Vue route parameters are not recognized within the function type

Seeking assistance on extracting parameters from my route in a vue page, I have the following implementation: <script lang="ts"> import { defineComponent } from 'vue'; import { useRoute } from 'vue-router'; export ...

What causes TypeScript to malfunction when using spread in components?

What is the reason for typescript breaking when props are sent to a component using a spread statement? Here's an example code snippet: type SomeComponentProps = Readonly<{ someTitle: string }> const SomeComponent = ({ someTitle }: SomeCompo ...

What is the best way to merge two approaches for tallying items within each category?

I have an Angular 8 application that includes two methods for displaying the number of items in each category. These items are retrieved from the back-end and are categorized as follows: <mat-tab> <ng-template mat-tab-label> ...

Migration of old AngularJS to TypeScript in require.js does not recognize import statements

I am looking to transition my aging AngularJS application from JavaScript to TypeScript. To load the necessary components, I am currently utilizing require.js. In order to maintain compatibility with scripts that do not use require.js, I have opted for usi ...

Can a ternary operator be used within an index type query when extending a partial type?

Can anyone provide a detailed explanation of the process unfolding in this snippet? I'm having trouble grasping how this code leads to a type declaration. type ModalErrors = Partial< { [key in keyof InputGroup]: InputGroup[key] extends Speci ...

What is the method in XState to trigger an event with the parameters send('EVENT_NAME', {to: 'a value from the context'})?

I want to send an event to a different spawned state machine using its ID, which I have stored as a string in a variable within the context. This state machine is neither the parent nor child. For example: context.sendTo = 'B_id' How can I use ...

Switching the checkbox state by clicking a button in a React component

Is there a way to update checkbox values not just by clicking on the checkbox itself, but also when clicking on the entire button that contains both the input and span elements? const options = ["Option A", "Option B", "Option C"]; const [check ...

Utilize key-value pairs to reference variables when importing as a namespace

Is it feasible to utilize a string for performing a lookup on an imported namespace, or am I approaching this the wrong way? Consider a file named my_file.ts with contents similar to: export const MyThing: CustomType = { propertyOne: "name", ...

Having trouble getting React app to recognize Sass properly

I have been working on developing a React app using TypeScript and the SASS preprocessor. Here is an example of my code: // Button.tsx import React from 'react'; import './Button.scss'; export default class Button extends React.Compone ...

Error in Mocha test: Import statement can only be used inside a module

I'm unsure if this issue is related to a TypeScript setting that needs adjustment or something else entirely. I have already reviewed the following resources, but they did not provide a solution for me: Mocha + TypeScript: Cannot use import statement ...

What could be causing the QullJS delta to display in a nonsensical sequence?

The outcome showcased in the delta appears as: {"ops":[{"retain":710},{"insert":" yesterday, and she says—”\n“The clinic?","attributes":{"prediction":"prediction"}},{"del ...

Tips for concealing information within the column labeled company Name with respect to the field designated as Company Name

I am currently working on an Angular 7 app and I am facing an issue: I cannot hide the data in the column for Company Name. The field "Name" in the report control JSON is labeled as Company Name. The report control is a table that contains various fields ...

Is it feasible to implement early-return typeguards in Typescript?

There are instances where I find myself needing to perform type checks on variables within a function before proceeding further. Personally, I try to minimize nesting in my code and often utilize early-return statements to keep the main functionality of a ...

Tips on exporting a basic TypeScript class in an Angular 4 module

I am facing a challenge with packaging a TypeScript class as part of an Angular module for exporting it as a library using ng-packagr. For instance, here is my class definition - export class Params { language: string ; country: string ; var ...

Is there a faster way to create a typescript constructor that uses named parameters?

import { Model } from "../../../lib/db/Model"; export enum EUserRole { admin, teacher, user, } export class UserModel extends Model { name: string; phoneNo: number; role: EUserRole; createdAt: Date; constructor({ name, p ...

Unexpected behavior encountered when using TypeScript type declarations

I am currently working on a Gatsby side project incorporating Typescript for the first time. I initially expected Typescript to behave similarly to PHP type declarations, but I have encountered some unforeseen issues. Despite feeling confident in my Typesc ...

Unable to inject basic service into component

Despite all my efforts, I am struggling to inject a simple service into an Angular2 component. Everything is transpiling correctly, but I keep encountering this error: EXCEPTION: TypeError: Cannot read property 'getSurveyItem' of undefined Even ...

Assuming control value accessor - redirecting attention

import { Component, Input, forwardRef, OnChanges } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Component({ selector: 'formatted-currency-input', templateUrl: '../v ...