Type-safe generic mapping with optional fields failing under strictNullChecks

In TypeScript I have a generic mapping service that requires a generic ComplexMappingType. This type defines which source field is to be mapped to which target field – a mapping definition. As input, this generic type requires a SourceType (S) and a TargetType (T). The goal is to create such mapping definitions with the related source and target fields in a typesafe way using this ComplexMappingType – it should work even for nested types. Here is my current solution:

type FieldMapping<S> = {
  source: keyof S;
};

// used for defining mappings with array properties, holding object values
type ArrayFieldMapping<S, T> = {
  baseSource: keyof S;
  itemMapping: ComplexMapping<S[keyof S] extends (infer U)[] ? U : never, T>;
};

type IsConfidenceType<T> = T extends { confidence: number } ? true : false;

// the mapping definition type
export type ComplexMapping<S, T> = {
  [K in keyof T]: T[K] extends Array<infer Item>
    ? Item extends object
      ? ArrayFieldMapping<S, Item>
      : never
    : T[K] extends object
      ? IsConfidenceType<T[K]> extends true
        ? FieldMapping<S>
        : {
            [P in keyof T[K]]: T[K][P] extends object
              ? ComplexMapping<S, T[K]>[P]
              : FieldMapping<S>;
          }
      : FieldMapping<S>;
};

Then you can create a mapping definition for i.e. the following specific Types:

type SourceType = {
  aSource: string;
  deliveryNotes: { name: string }[];
};

// removing the ? will make it work even in strictNullCheck=true
type TargetType = {
  aTarget: string;
  deliveryNotesTarget?: { lala: string }[]; 
};
 
// this can be defined in a typesafe way. You can not pick non existing properties
const mappingDefinition: ComplexMapping<SourceType, TargetType> = {
  aTarget: {
    source: "aSource",
  },
  deliveryNotesTarget: {
    // here the typescript error occurs if strictNullCheck is enabled
    baseSource: "deliveryNotes", 
    itemMapping: {
      lala: { // type safety is only here if i use the as ComplexMapping approach
        source: "name",
      },
    } as ComplexMapping<
      SourceType["deliveryNotes"][number],
      TargetType["deliveryNotesTarget"][number]

>,
  },
};

The solution works only if strictNullChecks is set to false in tsconfig. But if I set it to true, I got the following error on the baseSource property field:

Object literal may only specify known properties, and ‘baseSource’
does not exist in type ‘FieldMapping’.(2353) input.tsx(39,
3): The expected type comes from property ‘deliveryNotesTarget’ which
is declared here on type ‘ComplexMapping<SourceType, TargetType>’

Something I already figured out: there seems to be some issue with the property deliveryNotesTarget?. If you change it here to deliveryNotesTarget it works, but in my case it has also to work with the optional deliveryNotesTarget?. Setting strictNullChecks to false is also not an option.

Here is the code, which reproduces the issue:

Playground

3

It looks like your problem is that your conditional types of the form

T[K] extends Array<infer Item> ? ⋯ : T[K] extends object ? ⋯ : ⋯;

do not distribute over unions in the property type T[K]. You can fix this by refactoring to a utility type, so that the checked type is its own generic type parameter instead of T[K], resulting in distributive conditional types:

UtilityType<T[K]>

where

type UtilityType<TK> = TK extends Array<infer Item> ? ⋯ : TK extends object ? ⋯ : ⋯;

When you change a property from required to optional, then T[K] ends up gaining an undefined in its domain. So if T[K] were Array<X> when K is not optional, then it becomes Array<X> | undefined when K is optional.

But while Array<X> extends Array<infer Item> is true, (Array<X> | undefined) extends Array<infer Item> is false. So your code takes different paths for optional and required properties. So when deliveryNotesTarget is required you get

type CheckReq = ComplexMapping<SourceType, Required<TargetType>>;
/* type CheckReq = {
  aTarget: FieldMapping<SourceType>;
  deliveryNotesTarget: ArrayFieldMapping<SourceType, {
      lala: string;
  }>;
} */

but when it is optional you get

type Check = ComplexMapping<SourceType, TargetType>;
/* type Check = {
    aTarget: FieldMapping<SourceType>;
    deliveryNotesTarget?: FieldMapping<SourceType> | undefined;
}*/

because your check against Array failed.


I presume you’d like your types to distribute across unions, so that F<T | U | V> evaluates to F<T> | F<U> | F<V>, and that way F<Array<X> | undefined> will evaluate to F<Array<X>> | F<undefined>, and then the F<Array<X>> code path will still be taken. That also means you need to make sure F<undefined> does something reasonable, probably resulting in just undefined itself (but this is out of scope for the question as asked; I don’t want to dive into the code to figure out what you mean to do here, you need to figure that out yourself).

So let’s refactor your ComplexMapping<S, T> mapped type into something that just applies a utility type to its properties:

type ComplexMapping<S, T> = { [K in keyof T]: ComplexMappingProperty<S, T[K]> };

type ComplexMappingProperty<S, TK> =
  TK extends Array<infer Item> ? (Item extends object ? ArrayFieldMapping<S, Item> : never) :
  TK extends object ? (IsConfidenceType<TK> extends true ? FieldMapping<S> : {
    [P in keyof TK]: TK[P] extends object ? ComplexMapping<S, TK>[P] : FieldMapping<S>;
  }) :
  TK extends undefined ? undefined :
  FieldMapping<S>;

This is almost exactly the same as yours, except that ComplexMappingProperty<S, TK> is distributive in unions across TK, and that ComplexMappingProperty<S, undefined> is undefined (so we don’t have an extra FieldMapping<S> in there when T is undefined).

Now when deliveryNotesTarget is required nothing changes,

type CheckReq = ComplexMapping<SourceType, Required<TargetType>>;
/* type CheckReq = {
    aTarget: FieldMapping<SourceType>;
    deliveryNotesTarget: ArrayFieldMapping<SourceType, {
        lala: string;
    }>;
} */

but when it’s optional the same code path is taken so you get the expected type:

type Check = ComplexMapping<SourceType, TargetType>;
/* type Check = {
    aTarget: FieldMapping<SourceType>;
    deliveryNotesTarget?: ArrayFieldMapping<SourceType, {
        lala: string;
    }> | undefined;
} */

Looks good.


Note that there are other ways to proceed without using distributive conditional types. If you don’t care about unions other than | undefined then you can always use the NonNullable utility type to just remove undefined, so you check NonNullable<T[K]> extends instead of T[K] extends. Distribution across unions is usually what people expect, though.

Playground link to code

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