Ways to enforce a specific type based on the provided parameter

Scenario Background:

// Code snippet to do validation - not the main focus.
type Validate<N, S> = [S] extends [N] ? N : never;

// Note that by uncommenting below line, a circular constraint will be introduced when used in validateName().
// type Validate<N, S> = S extends N ? N : never;

// Validation function - implementation as JS (or return value) is not crucial.
// .. But it's important to use a Validate type with two arguments as shown above.
function validateName<N extends string, S extends Validate<N, S>>(input: S) {}

Issue: How can we provide only N without specifying S to the validateName (or Validate) mentioned above? The goal is to let S be inferred from the actual argument.

// Test.
type ValidNames = "bold" | "italic";

// Desired usage:
// .. But this is not possible due to "Expected 2 type arguments, but got 1."
validateName<ValidNames>("bold");   // Ok.
validateName<ValidNames>("bald");   // Error.

// Issue unresolved due to: "Type parameter defaults can only reference previously declared type parameters."
function validateName<N extends string, S extends Validate<N, S> = Validate<N, S>>(input: S) {}

Possible Solutions:

Solution #1: Assign input to a variable and use its type.

const input1 = "bold";
const input2 = "bald";
validateName<ValidNames, typeof input1>(input1);  // Ok.
validateName<ValidNames, typeof input2>(input2);  // Error.

Solution #2: Adjust the function to require an extra argument.

function validateNameWith<N extends string, S extends Validate<N, S>>(_valid: N, input: S) {}
validateNameWith("" as ValidNames, "bold");  // Ok.
validateNameWith("" as ValidNames, "bald");  // Error.

Solution #3: Use closure technique - wrap the function inside another.

// Function to create a validator and insert N into it.
function createValidator<N extends string>() {
    // Return the actual validator.
    return function validateName<S extends Validate<N, S>>(input: S) {}
}
const validateMyName = createValidator<ValidNames>();
validateMyName("bold");  // Ok.
validateMyName("bald");  // Error.

Updated: Revised the functions by eliminating the ambiguous :N[] part.

More Information / Context:

The aim is to develop a string validator for various uses, such as HTML class names. Most parts work seamlessly, except for the slightly complex syntax (refer to the 3 solutions provided above).

// Credits: https://github.com/microsoft/TypeScript/pull/40336
type Split<S extends string, D extends string> =
    string extends S ? string[] :
    S extends '' ? [] :
    S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] :
    [S];

// Type for validating a class name.
type ClassNameValidator<N extends string, S extends string, R = string> =
    Split<S, " "> extends N[] ? R : never;

// Function for validating classes.
function validateClass<N extends string, S extends ClassNameValidator<N, S>>(input: S) {}

const test3 = "bold italic";
const test4 = "bald";
validateClass<ValidNames, typeof test3>(test3);  // Ok.
validateClass<ValidNames, typeof test4>(test4);  // Error.

Answer №1

Here is a potential solution that may suit your needs. Instead of utilizing a Validation type, you can create a type that generates all permutations of possible values based on the given string union.

type AllPermutations<T extends string> = {
  [K in T]: 
    | `${K}${AllPermutations<Exclude<T, K>> extends infer U extends string 
        ? [U] extends [never] 
          ? "" 
          : ` ${U}` 
        : ""}` 
    | `${AllPermutations<Exclude<T, K>> extends infer U extends string 
        ? U 
        : never}`
}[T]

// Function for validating classes.
function validateClass<N extends string>(input: AllPermutations<N>) {}

It successfully passes the following set of tests.

type ValidNames = "bold" | "italic";

validateClass<ValidNames>("bold");  // Passes.
validateClass<ValidNames>("bold italic");  // Passes.
validateClass<ValidNames>("italic");  // Passes.
validateClass<ValidNames>("italic bold");  // Passes.
validateClass<ValidNames>("something else");  // Fails.

However, it's worth noting that as the union grows larger, performance may become an issue. I would recommend against using this approach with a larger union like ValidNames.

Playground

Answer №2

Resolution:

By chance, I stumbled upon a solution (or an elegant workaround). It follows the same core concept as workaround #3, but it is solely implemented in TypeScript without any additional JavaScript. The key approach is to use a "type closure" to separate the N and S components: essentially, defining a function with a generic N parameter on one side and utilizing the S exclusively on the other side.

// Defining a type for validation functions, separating N and S.
// .. Utilizing the ClassNameValidator specified in the original query.
type Validate<N extends string> = <S extends ClassNameValidator<N, S>>(input: S) => void;

// Subsequently, we can focus solely on N - excluding S.
// .. Notably, the actual JavaScript function can be reused (e.g., from a library).
const validateClass: Validate<ValidNames> = (_input) => {}

// Evaluation.
validateClass("bold");  // Success.
validateClass("italic bold");  // Success.
validateClass("bald");  // Failure.

It is important to highlight that this approach resolves the issue at hand for me - even though it may not perfectly align with the initial inquiry posed in the title (which appears to be currently unachievable).

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

Enhancing the NextPage Typescript Type: A Step-by-Step Guide

I'm working on a NextJS dashboard with role-based access control and I need to pass an auth object to each page from the default export. Here's an image showing an example of code for the Student Dashboard Home Page: Code Example of Student Dashb ...

Retrieving information from a JSON object in Angular using a specific key

After receiving JSON data from the server, I currently have a variable public checkId: any = 54 How can I extract the data corresponding to ID = 54 from the provided JSON below? I am specifically looking to extract the values associated with KEY 54 " ...

What kind of error should be expected in a Next.js API route handler?

Recently, I encountered an issue with my API route handler: import { NextRequest, NextResponse } from "next/server"; import dbConnect from "@/lib/dbConnect"; import User from "@/models/User"; interface ErrorMessage { mess ...

How can Angular components that are not directly related subscribe to and receive changes using a declarative approach?

Here's the issue at hand: I'm facing a problem with my Dashboard component. The Dashboard consists of a Sidebar and Navbar as child components. On the Sidebar, I have applied a custom permission directive to the links to determine if the user ha ...

Tips for centering an Angular mat prefix next to a label in a form field

Hey everyone, I need some help with aligning the prefix for an input with the mat label. Can anyone suggest a way to adjust the mat prefix so that it lines up perfectly with the mat label? Any assistance or ideas would be greatly appreciated. Here is the ...

Connecting extra parameters to an event listener

Scenario: I am facing a situation where my event handler is already receiving one parameter (an error object). However, I now need to pass an additional parameter when binding the event handler. I am aware of the bind() method, but I am concerned that it ...

"Implementing a loop to dynamically add elements in TypeScript

During the loop session, I am able to retrieve data but once outside the loop, I am unable to log it. fetchDetails(){ this.retrieveData().subscribe(data => { console.log(data); this.data = data; for (var k of this.data){ // conso ...

An issue occurred when attempting to use the Mailgun REST API from an Angular 6 application. The error message states that the 'from' parameter is missing

I am encountering an error that states the 'from' parameter is missing in the body. Can you help me troubleshoot why this issue is happening? export class EmailService { constructor(private http: HttpClient) {} sendMailgunMessage() { const opti ...

The function `Object.entries().map()` in TypeScript does not retain the original data types. What changes can I make to my interface to ensure it works correctly, or is there a workaround

Working with this interface: export interface NPMPackage { name: string; description: string; 'dist-tags': { [tag: string]: string; }; versions: { [version: string]: { name: string; version: string; dependencie ...

Create a const assertion to combine all keys from an object into a union type

I am working with an object similar to this (demo link): const locations = { city: {name: 'New York'}, country: {name: 'United States'}, continent: {name: 'North America'} } as const My goal is to create a union t ...

Step-by-step guide to initializing data within a service during bootstrap in Angular2 version RC4

In this scenario, I have two services injected and I need to ensure that some data, like a base URL, is passed to the first service so that all subsequent services can access it. Below is my root component: export class AppCmp { constructor (private h ...

Remove an element from an array within objects

Need help with removing specific items from an array within objects? If you want to delete all hobbies related to dancing, you may consider using the splice method const people = [{ id: 1, documents: [{ ...

Encountering an ERROR during the compilation of ./src/polyfills.ts while running ng test - Angular 6. The module build

I encountered a problem in an angular project I am working on where the karma.config was missing. To resolve this, I manually added it and attempted to run the test using the command ng test. However, during the execution, an error message appeared: [./src ...

javascript + react - managing state with a combination of different variable types

In my React application, I have this piece of code where the variable items is expected to be an array based on the interface. However, in the initial state, it is set as null because I need it to be initialized that way. I could have used ?Array in the i ...

The second guard in Angular 5 (also known as Angular 2+) does not pause to allow the first guard to complete an HTTP request

In my application, I have implemented two guards - AuthGuard for logged in users and AdminGuard for admins. The issue arises when trying to access a route that requires both guards. The problem is that the AdminGuard does not wait for the AuthGuard to fini ...

Using a union type annotation when passing into knex will result in the return of an unspecified

Knex version: 2.5.1 Database + version: postgres15 When passing a union typescript definition into knex as a type annotation, it returns the type any. However, by using type assertion as UserRecord, we can obtain the correct UserRecord type. It is my un ...

Dropdown Pattern with React CTA Modal

While using MaterialUI's ButtonGroup for a dropdown menu, I encountered an issue trying to set up a series of CTAs that are easily interchangeable within it. The goal is to have all components reusable and the choices in the dropdown dynamic. const C ...

Issue encountered while executing jest tests - unable to read runtime.json file

I've written multiple unit tests, and they all seem to pass except for one specific spec file that is causing the following error: Test suite failed to run The configuration file /Users/dfaizulaev/Documents/projectname/config/runtime.json cannot be r ...

Retrieving a list of actions triggered: Redux

In my Angular project, I am utilizing Redux to manage state and handle API calls. While exploring the redux devtools, I discovered a comprehensive list of executed actions. Is there a method to access a similar list of actions triggered within my angular a ...

How can I use the Required utility type in TypeScript for nested properties?

I'm exploring how to utilize the Required keyword to ensure that all members are not optional in TypeScript. I've achieved success with it so far, but I've run into an issue where it doesn't seem to work for nested members of an interfa ...