Narrowing only happens in particular circumstances.
If you perform a type guard on the property prop of an object obj, such as (typeof obj.prop === "string"), you might reasonably expect that if obj.prop is narrowed, then obj will also be narrowed. That is, if the type checker now knows that obj.prop is a string as opposed to string | null, then it should also know that obj is an object type with a prop property of type string as opposed to string | null. But generally speaking, this does not happen: if you perform a type guard on obj.prop, generally only obj.prop will be narrowed. The type of obj itself will stay stubbornly wide.
( An exception to this is when obj is of a discriminated union type and prop is a discriminant property. But TotoInt is not a union type at all, let alone a discriminated one, so checking a property of a TotoInt object will only possibly narrow that property and not the parent object. )
There is a suggestion at microsoft/TypeScript#42384 to propagate narrowings of properties up to their parent objects. But for now, this is not part of the language.
Therefore you need to work around it. The easiest workaround is to copy the narrowed property explicitly, since that property is properly narrowed:
if (toto.lastName) { const toto2: TotoInt2 = { ...toto, lastName: toto.lastName // <-- copy over the checked prop again }; // okay }
If you find yourself running into this issue often enough, you could write a helper user-defined type guard function that you call instead of doing the type check. It's sort of a do-it-yourself implementation of microsoft/TypeScript#42384, and is accordingly clunky:
function hasPropType<T, K extends keyof T, V extends T[K]>( obj: T, prop: K, guard: (v: T[K]) => v is V): obj is T & { [P in K]: V } { return guard(obj[prop]); }
The idea is that you check the property of key type K of an object of type T with a type guard function of type (v: T[K]) => v is V, where V is some narrower type than the known property type T[K]. And this will serve to narrow obj if the guard returns true.
For hasPropType, you'd like to check if toto.lastName is non-null, so you can write the following type guard function:
function isNonNullish<T>(x: T): x is NonNullable<T> { return x !== undefined && x !== null; }
(If you are using TS5.5 or greater you can leave off the x is NonNullable<T> type annotation because that can now be inferred from the body of the function. For TS5.4 or below you need to write this manually.) Now the check looks like:
if (hasPropType(toto, "lastName", isNonNullish)) { //const toto: TotoInt & { lastName: string; } const toto2: TotoInt2 = { ...toto }; // okay }
You can see that toto gets narrowed from TotoInt to TotoInt & {lastName: string}, which is assignable to TotoInt2.
Yes, that's clunky, but you might want this version if you have a reason why copying the property multiple times has bad side effects.
Playground link to code
lastNamecan be (and indeed is)nullintotoInt, but can't be intotoInt2.{ lastName: lastName: string | null }while the other has{ lastName: string }. TypeScript cannot guarantee that you're not assigningnullto a property that expects a string, and hence the warning.TotoIntisn't (fixed to adhere to standard TS naming conventions). You can work around it by copying the property again like this or if you're doing this a lot, building a type guard function like this.toto'slastNameproperty beMath.random() < 0.5 ? null : "abc"? People in the comments are getting hung up on the fact that we know the value isnull, but that doesn't seem to be the issue you're asking about. ... Please let me know if my prior comment fully addresses your question. If so, I could write up an answer. If not, what am I missing? (Please mention @jcalz if you reply so I'm notified).