Issue
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.
Solution
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).
Answered By - takaturre
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.