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.
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).