Without true regular expression validated types as discussed in microsoft/TypeScript#41160, what you are trying to do is impossible. There is no specific type DNACombinations
corresponding to strings that consist only of the characters "A"
, "C"
, "G"
, and "T"
.
The closest you can currently get is to make a generic type ValidDNACombinations<T>
that acts like a constraint on T
, so that T extends ValidDNACombinations<T>
if and only if T
would have been assignable to your desired DNACombinations
type. Here's one way to write that:
type ValidDNACombinations<T extends string, A extends string = ""> =
T extends `${infer F}${infer R}` ?
F extends DNAKeys ? ValidDNACombinations<R, `${A}${F}`> : `${A}${DNAKeys}` :
A;
That's a tail-recursive conditional type which uses template literal types to parse T
into individual characters, checking each one in turn against DNAKeys
. If every character matches then the output will just be the same as the input. Otherwise the output will be the longest initial segment of the input that matches, plus a final DNAKeys
character at the end. So a bad input will result in a good output which is "close" to it, to hopefully provide helpful error messages.
Then you could write const v: ValidDNACombinations<"CAT"> = "CAT"
, but that's redundant. So we can make a helper function asDNACombinations
that infers the type argument for you. Like this:
const asDNACombinations = <T extends string>(
t: T extends ValidDNACombinations<T> ? T : ValidDNACombinations<T>
) => t;
That's a bit complicated because TypeScript balks at a more straightforward recursive constraint like <T extends ValidDNACombinations<T>>(t: T) => t
. Anyway let's test it out:
const okay = asDNACombinations("GATTACA");
const bad = asDNACombinations("ATTACKING"); // error
// Argument of type '"ATTACKING"' is not assignable to parameter of type
// '"ATTACA" | "ATTACG" | "ATTACC" | "ATTACT"'.
So the good input is accepted, while the bad input is rejected with an error message that shows the problem.
Note that this still doesn't give you the desired behavior whereby splitting the string into characters is known to produce an array of DNAKey
values. The compiler is unable to perform the higher-order reasoning to know this. I'm not even sure if this would even be possible with regex types. So you'll need a type assertion or the like to tell the compiler about that:
const doSomethingWithDNA = <T extends string>(
dnaString: T extends ValidDNACombinations<T> ? T : ValidDNACombinations<T>) => {
for (const letter of dnaString) {
console.log(dna[letter as DNAKeys]); // <-- assertion needed
}
}
But at least it works from the caller's side:
doSomethingWithDNA("CAT"); // okay
doSomethingWithDNA("DOG"); // error
One final note, and this is mentioned in a write-up in microsoft/TypeScript#41160: these sorts of things only really help if your data is static, in the form of string literal types known at compile time, so your TypeScript code looks like doSomethingWithDNA("CAT")
where the "CAT"
string literal is in the code directly. If, on the other hand, your data is dynamic and never appears directly in the TypeScript code, so your TypeScript code looks like doSomethingWithDNA(getDNAFromSomewhere())
, where getDNAFromSomewhere()
returns a value of type DNACombinations
but not a string literal type, then it's really not very useful at all. You might as well just give DNACombinations
a nominal-ish type like the following branded type:
type DNACombinations = string & { __validDNA: true } & Iterable<DNAKeys>;
declare function getDNAFromSomewhere(): DNACombinations;
Then your doSomethingWithDNA
implementation works well enough even without an assertion (because I said that it was Iterable<DNAKeys>
):
const doSomethingWithDNA = (dnaString: DNACombinations) => {
for (const letter of dnaString) {
console.log(dna[letter]); // okay
}
}
And then as long as you don't care about hardcoded string literals, you'll be happy:
doSomethingWithDNA(getDNAFromSomewhere()) // okay
doSomethingWithDNA("CAT"); // error, but who cares, you won't do this
This doesn't require regex types or generics or anything, and you can do it today.
Playground link to code