Home > Net >  How to force a type to be defined by the given argument
How to force a type to be defined by the given argument

Time:07-30

Background situation:

// Type to do the validation - not so important.
type Validate<N, S> = [S] extends [N] ? N : never;

// Note that with below line, N will have a circular constraint when using from validateName().
// type Validate<N, S> = S extends N ? N : never;

// The function to validate - how it runs as JS (or even what it returns) is not important.
// .. However, it is important that it must use a Validate type with two arguments like above.
function validateName<N extends string, S extends Validate<N, S>>(input: S) {}

Problem: How to supplement only N but not S to the validateName (or Validate) above? We want S to remain inferred by the actual argument.

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

// Desired usage:
// .. But we can't do this due to "Expected 2 type arguments, but got 1."
validateName<ValidNames>("bold");   // Ok.
validateName<ValidNames>("bald");   // Error.

// Cannot solve like below 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) {}

Working workarounds:

Workaround #1: Store the input as a variable, and use its type.

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

Workaround #2: Make the function require 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.

Workaround #3: Use closure - by wrapping the function inside another.

// First a function to create a validator and put 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.

Edited: Modified the functions above by removing the confusing :N[] return part.

More info / the context:

I'm actually trying to build a string validator that can be used, eg. for html class names. Everything else works, except the usage is clunky (see the 3 workarounds above).

// Thanks to: 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 to validate a class name.
type ClassNameValidator<N extends string, S extends string, R = string> =
    Split<S, " "> extends N[] ? R : never;

// Function to validate class.
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.

CodePudding user response:

I might have a solution that works for you. Instead of having a Validation type, you could have a type that calculates all permutations of the 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 to validate class.
function validateClass<N extends string>(input: AllPermutations<N>) {}

It passes the following tests.

type ValidNames = "bold" | "italic";

validateClass<ValidNames>("bold");  // Ok.
validateClass<ValidNames>("bold italic");  // Ok.
validateClass<ValidNames>("italic");  // Ok.
validateClass<ValidNames>("italic bold");  // Ok.
validateClass<ValidNames>("something else");  // Error.

But if the union gets bigger, this will become perfomance hungry. I would not advise to use this if ValidNames is a larger union.

Playground

CodePudding user response:

Solution:

I accidentally found a solution (or a clean workaround). It's the same background idea as in workaround #3, but implemented purely in TS (no extra JS). The idea is to separate the N and S parts using a "type closure": namely, to define a function with a generic N param on the left hand side, and use the S only on the right hand side.

// Create a type for validation function, separating N and S.
// .. Using the ClassNameValidator from the original question.
type Validate<N extends string> = <S extends ClassNameValidator<N, S>>(input: S) => void;

// Then we can supplement just N - without S.
// .. Note that the actual js function can be reused (eg. from a library).
const validateClass: Validate<ValidNames> = (_input) => {}

// Test.
validateClass("bold");  // Ok.
validateClass("italic bold");  // Ok.
validateClass("bald");  // Error.

Note that this solves the problem for me - though it's not strictly what was asked in the title (which seems to be currently impossible).

  • Related