TypeScript object type narrowing
- Published
Sometimes you want to cast your objects to consts in TypeScript. Most of the time, it works fine. You create an object and append as const. Easy. Things become not so easy when you wish to cast an object to const whilst also adhering to a type. You create an object, append : YourType and then append as const. Queue the trumpet of disappointment.
For some (in)sane reason, creating constant objects that adhere to a type just doesn't work this way. The object will always be typed as the type, never as a const.
type Material = { a?: string; b?: string; c?: string }
const x: Material = { a: 'iron' } as const; // Material
Looking around, I couldn't find much on the internet. This seems like a basic feature that a language must have, surely? Am I just dense?
Fortunately, I remembered that generics exist. Generics have this very useful property where they automatically cast to constants. So if you create a function that returns a constrained generic...
type Material = { a?: string; b?: string; c?: string }
const x: Material = { a: 'iron' } as const; // Material
const material = <T extends Material>(x: T): T => x;
const y = material({ a: 'iron' }); // { a: string }
It doesn't create a const ({ a: "iron" }) sadly, but it does narrow the type. You can explore this for yourself on the TypeScript playground.
Update
Sage kindly reached out to me and explained that const type parameters are a thing, actually:
type Material = { a?: string; b?: string; c?: string }
const material = <const T extends Material>(x: T): T => x;
const y = material({ a: 'iron' }); // { readonly a: "iron" }
Using as const also works:
type Material = { a?: string; b?: string; c?: string }
const material = <T extends Material>(x: T): T => x;
const x = { a: 'iron' } as const
const y = material(x); // { readonly a: "iron" }
// Or
const z = material({ b: 'silver' } as const) // { readonly b: "silver" }
Somewhat confusing, const can mean two different things based on its context:
- This variable cannot be reassigned, e.g.
const y = 1 - Don't widen types and make read-only, e.g.
let x = { a: 'iron' } as const
const x = 1;
x = 2; // Error
let y = { id: "abc" };
y.id = "def"; // Error
let z = { id: "abc" };
z = 1; // Totally fine
As objects are not const by default, it makes sense that the generic is a narrowed Material.