Check type of constant without widening type?

Viewed 93

Is there a way to typecheck the value assigned to a variable without widening the type?

For example,

const a = {foo: "hi"} as const; // typeof = {foo: "hi"}

type THasFoo = {foo: string};
const b: THasFoo = {foo: "hi"} as const; // typeof = THasFoo

I'd like to check that whatever I assigned to a has member foo, but without losing the type information of what that value was.


Note: specifically I'm interested in the kind of checking that comes from direct assignment. E.g.

const a = {foo: "hi", bar: "world"} as const;

type THasFoo = {foo: string};
const b: THasFoo = a;

Passes. However,

type THasFoo = {foo: string};
const b: THasFoo = {foo: "hi", bar: "world"} as const;

Does not. It is the latter form of checking I would love to have (but without widening).

2 Answers

Preamble: I don't hold black belt in TypeScript so my answer might be sub-optimal (or even worse)

const b = (<T extends THasFoo>(v: T) => v)(a);

So, it creates an anonymous function and immediately invokes it. If instead of a variable the literal is passed - it would follow the same assignability rules (like: literals must have exactly the same properties) as if it was a direct assignment.

Drawback: it makes one unnecessary function call, but probably modern engines could inline or entirely eliminate it.

I'm unclear on how exactly you're trying to use this so maybe @zerkms answer is exactly what you are looking for, but my mind jumped to type guards. We define a function that checks if any object has a foo property which is a string. If the function returns true, then typescript narrows the type to include THasFoo without widening anything.

const hasFoo = <T extends {foo?: any}>(object: T): object is T & THasFoo => {
    return typeof object.foo === "string";
}

const a = {foo: "hi", bar: 42} as const;
if (hasFoo(a)) {
    console.log(a); // a gets refined to { readonly foo: "hi"; readonly bar: 42; } & THasFoo
}

const b = {foo: "hi", bar: 42};
if (hasFoo(b)) {
    console.log(b); // b gets refined to { foo: string; bar: number; } & THasFoo
}

Playground Link

Related