TypeScript deliberately infers literal types in various scenarios, but often broadens those types unless specified otherwise. One exception is when a type parameter extends
one of the literal types. The rule is that if you specify T extends string
, TypeScript preserves the exact literal value. This behavior also applies to unions, such as T extends Primitives
.
To ensure that (unions of) string, number, and boolean literals get widened to (unions of) string
, number
, and boolean
, we can utilize conditional types:
type WidenLiterals<T> =
T extends boolean ? boolean :
T extends string ? string :
T extends number ? number :
T;
type WString = WidenLiterals<"hello"> // string
type WNumber = WidenLiterals<123> // number
type WBooleanOrUndefined = WidenLiterals<true | undefined> // boolean | undefined
This approach works well, but there is an issue where conditional types are not properly checked in TypeScript. The problem arises when two structurally identical types are not mutually assignable, even though they should be.
If this poses a challenge for your use case, you can take another route by defining an unconstrained version like Data<T>
instead:
class Data<T> {
constructor(public val: T) {}
set(newVal: T) {
this.val = newVal;
}
}
Then you can relate the type and value PrimitiveData
to Data
as follows:
interface PrimitiveData<T extends Primitives> extends Data<T> {}
const PrimitiveData = Data as new <T extends Primitives>(
val: T
) => PrimitiveData<WidenLiterals<T>>;
With this implementation, instances created using PrimitiveData
will have a widened type:
const b = new PrimitiveData("hello"); // PrimitiveData<string>
b.set("world"); // okay
let a = new PrimitiveData("goodbye"); // PrimitiveData<string>
a = b; // okay
This alternative may offer more convenience for users of PrimitiveData
, despite requiring some workarounds on the implementation side.
Hopefully, one of these approaches helps you progress with your TypeScript project. Best of luck!
Code Link