I want to create a function which does something for a specific type and leave it alone for all other cases. Something like this:
function trim<T>(input: T): T {
if (typeof input === 'string')
input = input.trim();
return input;
}
But typescript complains on assignment in line-3:
Type 'string' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'string'.ts(2322)
Since input
is string
in the if
block, input
should be assignable to string
but it's not working. What is the better way to express this?
CodePudding user response:
The issue is that when Typescript is provided with a generic then it should work for all types. This includes literal types such as:
function trim<T>(input: T): T {}
trim("abc") // typescript expects the string literal "abc" not generic string
trim(1) // typescript expects the number 1. not a generic number.
// similarly for:
trim("abc ") // typescript expects "abc " not "abc".
The way that I would proabably recommend solving is by defining a conditional return type. However there are existing issues with conditional return types. As such this will be implemented as an overload:
function trim<T>(input: T): T extends string ? string : T;
function trim(input: any): any {
if (typeof input === 'string') {
return input.trim();
}
return input;
}
CodePudding user response:
Since you are trying to infer literal type, I thought this approach might be interesting.
Let's try to implement type utility which will emulate String.prototype.trim
The
trim()
method removes whitespace from both ends of a string and returns a new string
So, it should remove all whitespaces from both start and end of the stirng, but not from the middle.
Consider this utility type:
type WhiteSpace = ' ' | '\n'
type TrimStart<T extends string, Trimmed extends string = ''> =
// string destructure
// Char represents first string character
// Rest representa rest characters
T extends `${infer Char}${infer Rest}`
// if Trimmed is empty string it means that we are
// at the start because no character has been added to it
? (Trimmed extends ''
// if we are at the beginning and Char is a white space
? (Char extends WhiteSpace
// call it recursively with Rest (without WhiteSpace)
? TrimStart<Rest, Trimmed>
// otherwise call it recursively and add charater to Trimmed
: TrimStart<Rest, `${Trimmed}${Char}`>)
: TrimStart<Rest, `${Trimmed}${Char}`>)
: Trimmed
{
type _ = TrimStart<'a b'> // "a b"
type __ = TrimStart<' ab'> // "ab"
type ___ = TrimStart<' ab\n'> // "ab\n"
type ____ = TrimStart<'\n ab\n'> // "ab\n"
type _____ = TrimStart<'\n a\nb\n'> // "a\nb\n"
type ______ = TrimStart<'\n a\nb \n '> // "a\nb \n "
}
TrimStart
recursively iterates through the string and removes all white spaces (see WhiteSpace type) from the start. You will find description in the comments.
This is not the end. We still need to do the same but for the end of a string. TO avoid complexity, I decided reuse this utility type. So in order to reuse it, I need to reverse the string, apply TrimStart
and reverse it again.
See this:
type Reverse<
T extends string,
Reversed extends string = ''
> =
T extends `${infer Char}${infer Rest}`
? Reverse<Rest, `${Char}${Reversed}`>
: Reversed
type Trim<T extends string> = Reverse<TrimStart<Reverse<TrimStart<T>>>>
Here you can find whole example:
type WhiteSpace = ' ' | '\n'
type TrimStart<T extends string, Trimmed extends string = ''> =
// string destructure
// Char represents first string character
// Rest representa rest characters
T extends `${infer Char}${infer Rest}`
// if Trimmed is empty string it means that we are
// at the start because no character has been added to it
? (Trimmed extends ''
// if we are at the beginning and Char is a white space
? (Char extends WhiteSpace
// call it recursively with Rest (without WhiteSpace)
? TrimStart<Rest, Trimmed>
// otherwise call it recursively and add charater to Trimmed
: TrimStart<Rest, `${Trimmed}${Char}`>)
: TrimStart<Rest, `${Trimmed}${Char}`>)
: Trimmed
{
type _ = TrimStart<'a b'> // "a b"
type __ = TrimStart<' ab'> // "ab"
type ___ = TrimStart<' ab\n'> // "ab\n"
type ____ = TrimStart<'\n ab\n'> // "ab\n"
type _____ = TrimStart<'\n a\nb\n'> // "a\nb\n"
type ______ = TrimStart<'\n a\nb \n '> // "a\nb \n "
}
type Reverse<
T extends string,
Reversed extends string = ''
> =
T extends `${infer Char}${infer Rest}`
? Reverse<Rest, `${Char}${Reversed}`>
: Reversed
type Trim<T extends string> = Reverse<TrimStart<Reverse<TrimStart<T>>>>
{
type _ = Trim<'a b'> // "a b"
type __ = Trim<' ab'> // "ab"
type ___ = Trim<' ab\n'> // "ab"
type ____ = Trim<'\n ab\n'> // "ab"
type _____ = Trim<'\n a\nb\n'> // "a\nb"
type ______ = Trim<'\n a\nb \n '> // "a\nb"
}
function trim<T extends string>(input: T): Trim<T>
function trim<T>(input: T): T
function trim<T>(input: T) {
if (typeof input === 'string') {
return input.trim();
}
return input;
}
const str = `hello
world
`
const a = trim("abc ") // "abc"
const b = trim(" abc \n") // "abc"
const c = trim("a b c") // "a b c"
const newLine = trim(str) // "hello\nworld"
const d = trim(1) // 1
While this solution might look interesting, it should be thoroughly tested with all kinds of white spaces. Hence using just string
type instead of Trim<T>
might be safer.
function trim<T extends string>(input: T): string
function trim<T>(input: T): T
function trim<T>(input: T) {
if (typeof input === 'string') {
return input.trim();
}
return input;
}
Playground Please keep in mind that you want to overload your function you should always add min 2 signatures
The signature of the implementation is not visible from the outside. When writing an overloaded function, you should always have two or more signatures above the implementation of the function.