Issue with TypeScript and spread operator

I don’t understand why this code:

interface TotoInt {
    name: string;
    lastName: string | null;
}

interface TotoInt2 {
    name: string;
    lastName: string;
}

const toto: TotoInt = {
    name: 'toto',
    lastName: Math.random() < 0.5 ? null : "abc",
};

if (toto.lastName) {
    const toto2: TotoInt2 = {
        ...toto,
    };
}

produces the following output:

I would have expected TypeScript to understand that by checking if (toto.lastName), toto.lastName would be guaranteed to be non-null, thus allowing the usage of TotoInt2.

If I do it this way instead (with the non-null assertion exclamation mark operator), TypeScript doesn’t complain:

    // if (toto.lastName) {
    //     const toto2: TotoInt2 = {
    //         ...toto,
    //     };
    // }

    const toto2: TotoInt2 = {
        name: toto.name,
        lastName: toto.lastName!,
    };

Is this an issue with the way TypeScript (the version I use is 4.8.3) handles the spread operator? Is there no way around the full reconstruction of an object literal with the ! non nullable operator to make the code accept the usage of TotoInt2?

The object is quite simple for demo purposes, but I’m working with a big object, that ideally I could pass into a function that would check for null values and that I thus wouldn’t have to reconstruct entirely with a new object literal and ! non nullable operators.

14

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

0

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật