Reputation: 9013
I have a use-case where I'm building to a target "state":
type State = { foo: number, bar: number, baz?: string };
When I start, i most likely haven't achieved the full State but rather am happy to fit into the shape of Partial<State>
. What I want to be able to do, how is infer when I've reached fabled State
state.
To help me achieve this goal, I first created a utility type called Resolve:
type Resolve<T extends Partial<State>> =
T extends infer U
? U extends State ? State : Partial<State>
: never;
This utility works when it is given inferred types like this:
const implicit1 = { foo: 5 };
const implicit2 = { foo: 5, bar: 10 };
// YAY: resolves to Partial<State>
const implicitResolve1: Resolve<typeof implicit1> = implicit1;
// YAY: resolves to State
const implicitResolve2: Resolve<typeof implicit2> = implicit2;
However, once a type has been expressed as a Partial<State>
it refuses to infer that it might be of type State
:
const explicit1: Partial<State> = { foo: 5 };
const explicit2: Partial<State> = { foo: 5, bar: 10 };
// YAY: correctly resolves to Partial<State>
const explicitResolve1: Resolve<typeof explicit1> = explicit1;
// SAD FACE: also resolves to Partial<State> even though the intent
// was for it to be recognized that it is a valid State!
const explicitResolve2: Resolve<typeof explicit2> = explicit2;
In my broader solution I had a type guard already waiting in the wings and I figured that would give me the super powers I'd need:
type TypeGuard<T> = (thing: unknown) => thing is T;
const tg: TypeGuard<State> = (thing: unknown): thing is State => {
return typeof thing === "object"
&& typeof (thing as any)?.foo === "number"
&& typeof (thing as any)?.bar === "number";
};
function SuperResolve(thing: unknown, tg: TypeGuard<State>) {
return tg(thing) ? thing as State : thing as Partial<State>
}
// SHOCKED: this resolves to Partial<State> too!
const fnResolve = SuperResolve(explicit2, tg);
I'm now at the end of my abilities ... surely there is some way to detect when a Partial<T>
has reached <T>
.
Upvotes: 2
Views: 935
Reputation: 328362
If you annotate a variable to be a single object type like Partial<State>
(equivalent to { foo?: number, bar?: number, baz?: string }
), the TypeScript compiler will not narrow the apparent type of the variable upon assigning a more specifically-typed value to it.
Such control-flow-based narrowing only happens to variables/properties of union types, and Partial<State>
is not itself a union type.
So, as soon as you write
const x: Partial<State> = { foo: 5, bar: 10 };
you have discarded any information the compiler might have had about the particular value you assigned to x
. The variable x
is of type Partial<State>
no matter what you assign to it. You could use a user-defined type guard and control flow analysis to conditionally narrow it, but this has nothing to do with the actual value assigned to x
:
if (tg(x)) {
x.bar.toFixed(2); // okay
}
If you want the compiler to remember that x
is assignable to State
, you should not preemptively widen it to Partial<State>
. Just let the compiler infer the type of x
:
const x = { foo: 5, bar: 10 }; // okay
Anything that requires a State
or a Partial<State>
will be happy to accept x
due to TypeScript's structural type system:
function takeState(state: State) { }
takeState(x); // okay
If you want to ensure that x
is really a Partial<State>
upon assignment and not only catch an error later, you could use a helper function like
const asPartialState = <T extends Partial<State>>(t: T) => t;
And verify that it checks the type of its input against Partial<State>
without widening it to Partial<State>
:
const y = asPartialState({ foo: 5, bar: 10 }); // okay
takeState(y); // okay
const z = asPartialState({ foo: 5, bar: "oops", baz: "" }); // error!
// ------------------------------> ~~~
// Type 'string' is not assignable to type 'number | undefined'.
None of this would allow you to do any "building" of a Partial<State>
into State
, since the type of a variable does not mutate upon assignments (I don't see any such building in your question either, so I'm not sure if that is in or out of scope. Assuming it's in scope.)
So, this won't work:
const x = { foo: 5 }
x.bar = 10; // error!
takeState(x); // error!
const y: Partial<State> = { foo: 5 }
y.bar = 10; // okay
takeState(y); // error!
If you are going to do this building by manually assigning each property, you can use something like an assertion function instead to get control-flow narrowing:
function setProp<T extends object, K extends PropertyKey, V>(
obj: T, key: K, val: V
): asserts obj is T & Record<K, V> { (obj as any)[key] = val }
const x = { foo: 5 }
setProp(x, "bar", 10); // okay now
takeState(x); // okay
If you are hoping to do some more complex or abstract "building" where you loop over properties or pass things to other functions which are not themselves assertion functions, even this will not work:
const x = {};
(["foo", "bar"] as const).forEach(k => setProp(x, k, 5));
takeState(x); // error!
In such cases, you should just give up on having the compiler try to follow the relatively complicated control flow that proves when your Partial<State>
has grown into a State
, and just assert that it's been done (with possible loss of type safety):
takeState(x as State); // no error, but maybe you're wrong
or do an unnecessary runtime check with your type guard function:
if (!tg(x)) throw new Error("My life is a lie");
takeState(x); // okay
Upvotes: 2
Reputation: 2656
State
is a complete subset of Partial<State>
, that is why State | Partial<State> == Partial<State>
.
The only way you can coerce the Partial<State>
into State
is by explicit setting Required<State>
or, using type-fest and setting SetRequired<Partial<State>, "foo" | "bar">
(but this one implies that you can extract the keys from somewhere).
The following code:
const explicit2: Partial<State> = { foo: 5, bar: 10 };
const explicitResolve2: Resolve<typeof explicit2> = explicit2;
is an error because you stripped all the information regarding obligations when you set : Partial<State>
, it doesn't matter what exists after the =
.
I suggest you get two Type Guards to assert the types you need. One that can tell if the argument is a Partial<State>
and another to tell if it is an State
, and run them in different if
statements.
Upvotes: 0