Building off of this question (no I do not have a grand scheme, I simply thought of this after getting my question answered.):
I have the following:
type LengthOfString<T extends string, L extends any[] = []> =
T extends `${string}${infer R}`
? LengthOfString<R, [...L, string]>
: L["length"]
type StringOfLength<T extends string, N extends number> = LengthOfString<T> extends N ? T : never
function isStringOfLength<T extends string, N extends number>(str: T, len: N): str is StringOfLength<T, N> {
return str.length === len
}
function takeString<T extends string>(str: StringOfLength<T, 5>) {
if (!isStringOfLength<T, 5>(str, 5)) throw Error("String length !== 5")
console.log(str)
}
// Works! (fresh)
takeString("22222")
// As expected, errors (fresh)
takeString("2222")
let str = "22222"
// Errors -- how can I avoid this? (str isn't fresh)
takeString(str)
As shown in the code, I have a run-time type guard isStringOfLength that is meant to throw runtime errors for non-fresh variables. However, I am unsure about how to change my code to accept these non-fresh values, while type-checking against the fresh strings as shown with the first two calls.
>Solution :
If we want to accept
-
valid literal string types
-
just the
stringtype
but don’t want to accept
- invalid literal string types
we can add an additional check to the function parameter.
function takeString<
T extends string
>(str: string extends T ? T : StringOfLength<T, 5>) {
if (!isStringOfLength<T, 5>(str, 5)) throw Error("String length !== 5")
console.log(str)
}
We accept T as an argument to the function if string extends T. But if T is a string literal, the argument’s type must be StringOfLength<T, 5>. This might seem counter-intuitive as we check if string extends T before we even infer T but the compiler makes it work for us.
// Works! (fresh)
takeString("22222")
// As expected, errors (fresh)
takeString("2222")
let str = "22222"
// Does not error anymore
takeString(str)