I'm trying to create wrappers for primitive types with possible nullable values if specified. But encountered a problem: TS automatically narrows type to provided value. It can be bypassed by manual type specification in generic, but it looks kind of ugly for the main use case.
class Wrapper<T> {
constructor(
public value: T
) { }
}
class StringWrapper<T extends string | null = string> extends Wrapper<T> {
}
new Wrapper(`a`); // Wrapper<string> - Perfect
new StringWrapper(`a`); // StringWrapper<'a'> - Too narrowed
new StringWrapper<string>(`a`); // StringWrapper<string> - Ugly
Is there are possibility to avoid narrowing to literal and make such cases possible?
type TClock = `Tic` | `Tac`;
new StringWrapper(1); // TS error
new StringWrapper(`a`); // StringWrapper<string>
new StringWrapper<TClock>(`Tic`); // StringWrapper<TClock>
new StringWrapper<TClock | null>(null); // StringWrapper<TClock | null>
CodePudding user response:
A generic constraint that includes string
, like T extends string | null
serves as a hint to the compiler that it should infer a string literal type for T
if possible. This is intended behavior, as implemented and described in microsoft/TypeScript#10676.
So if you want to avoid such narrowing, you can't constrain T
to string | null
. One alternative approach is not to constrain T
at all, but anywhere you were using a value of type T
, you use a value of the intersection T & (string | null)
. So any part of T
that isn't assignable to string | null
will be removed (e.g., (number) & (string | null)
reduces to never
). This has much the same effect as a constraint, without the inference behavior:
class StringWrapper<T,> extends Wrapper<T & (string | null)> { }
So now everything behaves as you wanted in terms of inference:
type TClock = `Tic` | `Tac`;
new StringWrapper(`a`); // StringWrapper<string>
new StringWrapper<TClock>(`Tic`); // StringWrapper<TClock>
new StringWrapper<TClock | null>(null); // StringWrapper<TClock | null>
but the compiler still rejects constructor arguments which would have violated your intended constraint:
new StringWrapper(3) // error