HaveSpacesuit
HaveSpacesuit

Reputation: 4014

Advanced TypeScript extending a type with required and optional properties

Here is a simple case that is easy in isolation, but breaks down when composed into a larger type.

If scrollToItem is defined, then getRowId must also be defined. Otherwise, getRowId is optional.

This type definition works just fine:

type ScrollProps<T> =
  | { scrollToItem: T; getRowId: (row: T) => string; }
  | { getRowId?: (row: T) => string; };

const a: ScrollProps<number> = {}; // ok
const b: ScrollProps<number> = { scrollToItem: 6, getRowId: (row) => '' }; // ok
const c: ScrollProps<number> = { getRowId: (row) => '' }; // ok
const d: ScrollProps<number> = { scrollToItem: 6 }; // error - expected

It can be extended further to pick the getRowId property from an imported library type.

type LibraryObject<T> = {
  getRowId?: (row: T) => string;
  otherProps?: boolean;
};

type ScrollProps<T> =
  | ({ scrollToItem: T } & Required<Pick<LibraryObject<T>, 'getRowId'>>)
  | Pick<LibraryObject<T>, 'getRowId'>;

const a: ScrollProps<number> = {}; // ok
const b: ScrollProps<number> = { scrollToItem: 6, getRowId: (row) => '' }; // ok
const c: ScrollProps<number> = { getRowId: (row) => '' }; // ok
const d: ScrollProps<number> = { scrollToItem: 6 }; // error - expected

Unfortunately, I have to add this type definition onto an existing type, which is already extending the library type. For some reason, TypeScript no longer requires getRowId when passing scrollToItem IF ALSO passing some of the other properties it extends.

type LibraryObject<T> = {
  getRowId?: (row: T) => string;
  otherProps?: boolean;
};

type ScrollProps<T> =
  | ({ scrollToItem: T } & Required<Pick<LibraryObject<T>, 'getRowId'>>)
  | Pick<LibraryObject<T>, 'getRowId'>;

type MasterType<T> = LibraryObject<T> & ScrollProps<T>;

const a: MasterType<number> = {}; // ok
const b: MasterType<number> = { scrollToItem: 6, getRowId: (row) => '' }; // ok
const c: MasterType<number> = { getRowId: (row) => '' }; // ok
const d: MasterType<number> = { scrollToItem: 6 }; // error - expected
const e: MasterType<number> = { scrollToItem: 6, otherProps: true }; // NO ERROR, but I wish there were! 🙁

Why would setting any of the other properties negate the required properties managed by ScrollProps?

Upvotes: 1

Views: 49

Answers (1)

HaveSpacesuit
HaveSpacesuit

Reputation: 4014

The comments on this question led me to an answer. I don't fully understand, but TypeScript seems to allow partial implementation of Type contracts. Updating the ScrollProps type to use Partial<Record<'scrollToItem', never>> in the second union option gives the expected results.

type ScrollProps =
  | ({ scrollToItem: number } & Required<Pick<LibraryObject, 'getRowId'>>)
  | (Partial<Record<'scrollToItem', never>> & Pick<LibraryObject, 'getRowId'> );

const d: MasterType = { scrollToItem: 6 }; // error - expected
const e: MasterType = { otherProps: true, scrollToItem: 6 }; // gives an error now

Upvotes: 1

Related Questions