Home > OS >  Avoid narrowing to literal
Avoid narrowing to literal

Time:01-14

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

Playground link to code

  • Related