Changelog - 2026-10-09
Const values stay literal
Bug Fix Breaking ChangeIn one line. A value typed with TConstValue<typeof X> keeps its literal union wherever it goes.
typescript
class Periods {
static readonly HALF_DAY = '12h';
static readonly DAY = '24h';
}
type TPeriod = TConstValue<typeof Periods>; // '12h' | '24h'
const rows: { value: TPeriod }[] = [{ value: Periods.HALF_DAY }, { value: Periods.DAY }];
const mapped = rows.map(row => ({ value: row.value }));
const typed: { value: TPeriod }[] = mapped; // compiled only after this fixWhat changed
- The three aliases end in
& {}.TConstValue,TStringConstValueandTNumberConstValuein@venizia/ignis-helpers, andTConstValuein@venizia/ignis-inversion, have the same members as before. - The members no longer widen. Before, a value of these types became
stringornumberinside an object literal, in alet, or in a method's inferred return type. - Nothing else moves. Class members are still assignable (
Periods.DAY). The union still works asRecordkeys, withsatisfies, in overloads and in exhaustiveswitches. In generic code it converts both ways withExtract<ValueOf<T>, ...>.
Who is affected
- Code that copied the value into an object or a
let. The type is now the literal union, as it always should have been. Casts or explicit return types added to work around the widening can go. - A generic inferred from one of these values, then given a plain
string. It no longer compiles. In practice this is a test:expect(value).toBe(someString). See below. - Everyone else. No action needed.
Breaking changes
WARNING
A generic inferred from a TConstValue value is now the literal union, so a plain string argument no longer fits it.
Before:
typescript
for (const [unit, expected] of table) {
// unit: TConstValue<typeof DurationUnits>, expected: string
expect(unit).toBe(expected);
}After:
typescript
for (const [unit, expected] of table) {
expect<string>(unit).toBe(expected);
}Give the generic the wider type, as above, or type the expected value with the same alias.
Details
- A
static readonlyinitializer gives each member a widening literal type.Extract<ValueOf<T>, ...>passed that type through unchanged, so TypeScript widened it at the first mutable position. Intersecting with{}makes it a regular literal union with the same members. - A method that returned one of these values in an object, with no return type, widened it too. An explicit return type added for that reason still compiles, and is no longer required.
Files: packages/helpers/src/common/types/const-value.ts, packages/inversion/src/common/types.ts
See Const value extraction types for the reference.