I am new to TypeScript.
I have some inherited code that, for some reason, uses the TypeScript literal string type. I have extracted an MWE (playground) below:
function coverTest(s: string) {
type vowel = 'a' | 'e' | 'i' | 'o' | 'u'
for (let c of s) {
// is there any way to coerce the type of c to be 'vowel'?
console.log(<vowel>c) // line 2: this does _not_ fail
}
}
coverTest('abc')
As documented here, it seems to me (from line 2 in above code) that the custom type information is not available at run time and hence there is no way to make the code fail at run time. Is my understanding correct?
If yes, what are my options to ensure that a string (like 'abc'
passed to a call to coverTest
) only consists of values of the custom vowel
type? Preferably, I want to keep the definition of the type vowel
as above, but reject the strings that contain non-vowels.
Update (after a day)
Looking at the answers (thanks, by the way!), it appears to me that I am looking for a way to get, at run time, all the literal values that a custom type (using unions by '|'
) allows (almost like an enum
). In other words, keeping the definition of the vowel
type above the same, is there a way to get a list of all its literal values at run time?
CodePudding user response:
Here's a way to use a modified version of the conditional type suggested by CertainPerformance to determine whether the string argument provided to the function matches the vowel pattern or not. If it does, the string is allowed and inferred as a literal. If it doesn't, it is rejected as not being assignable to never
.
I've also included an alternate assertion approach (asserting the iterable directly) to demonstrate how the compiler can infer the iterated values. This removes the need to continue to assert that
c
isVowel
at every usage site inside the loop.
// Derive the Vowel type from data (sorted array of vowels):
const vowels = ['a', 'e', 'i', 'o', 'u'] as const;
type Vowel = typeof vowels[number];
// Refactor of your exmaple:
type VowelStr<T> = T extends Vowel ? T
: T extends `${Vowel}${infer R}` ? R extends VowelStr<R> ? T
: never : never;
function coverTest <S extends string>(s: VowelStr<S>) {
for (const c of s as Iterable<Vowel>) {
console.log(c);
}
}
coverTest('iou'); // ok
coverTest('abc'); /*
~~~~~
Argument of type 'string' is not assignable to parameter of type 'never'.(2345) */
// Validation at runtime:
function assertIsVowelString <S extends string>(str: S): void {
const sortedUniques = [...new Set(str)].sort().join('');
if (!vowels.join('').includes(sortedUniques)) throw new Error(`"${str}" is not a vowel string`);
}
const abc = 'abc';
assertIsVowelString(abc); // will throw Error
const iou = 'iou';
assertIsVowelString(iou); // ok
CodePudding user response:
There's a way to do this with generics and conditional types.
type Vowel = 'a' | 'e' | 'i' | 'o' | 'u';
type PickVowels<S extends string, Acc extends string = ''> = S extends `${Vowel}${infer Rest}`
? S extends `${infer Char}${Rest}`
? PickVowels<Rest, `${Acc}${Char}`>
: never
: Acc;
type T1 = PickVowels<'abcde'>; // 'ae'
type T2 = PickVowels<'aeiouiu'>; // 'aeiouiu'
type T3 = PickVowels<'zxcvbnmvxcmv'>; // ''
function operateOnVowelOnlyString<S extends string>(s: S extends PickVowels<S> ? S : never) {}
operateOnVowelOnlyString('abcd'); // Error
operateOnVowelOnlyString('aaa'); // Ok
operateOnVowelOnlyString('aeiou'); // Ok
// Note this only works with string literals so this would error out because its type is inferred as `string` not `aeiou`
let s = 'aeiou';
operateOnVowelOnlyString(s);
CodePudding user response:
You would need regex-validated strings for arbitrary length strings.
Here's a solution for strings of length 1 to 6:
type vowel = 'a' | 'e' | 'i' | 'o' | 'u' | 'y';
type vowelstring =
`${vowel}`
| `${vowel}${vowel}`
| `${vowel}${vowel}${vowel}`
| `${vowel}${vowel}${vowel}${vowel}`
| `${vowel}${vowel}${vowel}${vowel}${vowel}`
| `${vowel}${vowel}${vowel}${vowel}${vowel}${vowel}`;
function coverTest(s: vowelstring)
{
for (let c of s)
{
console.log(c);
}
}
coverTest('aiou');
coverTest('abcd');