question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Improve typings related to partial object returns

See original GitHub issue

Describe the bug

The QueryDatabaseResponse type as defined in api-endpoints.d.ts does not expose the properties field on the objects within the results array. The results array is typed as a large union which does not allow type discrimination to narrow the result type.

To Reproduce Node version: 14 Notion JS library version: 1.0.4 Typescript version: 4.4.5

This snippet will generate the type issue - this is intended for reproduction of the typing issue, and not as validly executable code.

import { Client } from '@notionhq/client';

const client = {} as Client;
const data = await client.databases.query({ database_id: 'foo' });
const resultObject = data.results[0];
if (resultObject.object === 'page') { 
  resultObject.properties; // <-- a type error is generated here
}

TS error:

TS2339: Property 'properties' does not exist on type '{ parent: { type: "database_id"; database_id: string; } | { type: "page_id"; page_id: string; } | { type: "workspace"; workspace: true; }; properties: Record<string, { type: "number"; number: number | null; id: string; } | ... 17 more ... | { ...; }>; ... 9 more ...; url: string; } | { ...; }'.   Property 'properties' does not exist on type '{ object: "page"; id: string; }'.

Expected behavior

I would expect optional properties that are available on the returned results objects to be typed as optional, or a field to be present which can be used to discriminate the union, and allow access to the properties field.

Additional context

The type is defined as such:

export declare type QueryDatabaseResponse = {
    type: "page";
    page: EmptyObject;
    object: "list";
    next_cursor: string | null;
    has_more: boolean;
    results: Array<{
        parent: {
            type: "database_id";
            database_id: IdRequest;
        } | {
            type: "page_id";
            page_id: IdRequest;
        } | {
            type: "workspace";
            workspace: true;
        };
        properties: Record<string, { /** truncated here */}>;
        /** removed some properties here for brevity */
        object: "page";
        id: string;
        created_time: string;
        last_edited_time: string;
        archived: boolean;
        url: string;
    } | {
        object: "page";
        id: string;
    }>;
};

I believe that the last union is the issue - as it has the effect of removing all the previously declared types, and because object: "page"; is shared by both types in this union there is no property that can be used as a discriminator, in order to get access to the other fields.

Two solutions that I see:

  1. add a type discriminator to the response
  2. modify this type such that fields that may be optional are defined as optional. For example:
export declare type QueryDatabaseResponse = {
    type: "page";
    page: EmptyObject;
    object: "list";
    next_cursor: string | null;
    has_more: boolean;
    results: Array<{
        object: "page";
        id: string;
        parent?: {
            type: "database_id";
            database_id: IdRequest;
        } | {
            type: "page_id";
            page_id: IdRequest;
        } | {
            type: "workspace";
            workspace: true;
        };
        properties?: Record<string, { /** truncated here */}>;
        /** removed some properties here for brevity */
        created_time?: string;
        last_edited_time?: string;
        archived?: boolean;
        url?: string;
    }>;
};
    

I think 2 is simpler as it does not require any changes to the underlying api - whereas 1 may require an extra field be returned to allow type discrimination.

Issue Analytics

  • State:closed
  • Created a year ago
  • Reactions:22
  • Comments:7 (1 by maintainers)

github_iconTop GitHub Comments

6reactions
LordZardeckcommented, Apr 18, 2022

This seems to be working for me, maybe this will help somebody:

type PageResult = Extract<QueryDatabaseResponse['results'][number], {parent: {}}>;
type PageProperty<T> = Extract<PageResult["properties"][string], { type: T }>;
2reactions
lao9commented, Jul 27, 2022

Hi! In our last release we added some helper and utility functions.

https://github.com/makenotion/notion-sdk-js#typescript

Here is an example:

const fullOrPartialPages = await notion.databases.query({
  database_id: "897e5a76-ae52-4b48-9fdf-e71f5945d1af",
})
for (const page of fullOrPartialPages) {
  if (!isFullPage(page)) {
    continue
  }
  // The page variable has been narrowed from PageObjectResponse | PartialPageObjectResponse to PageObjectResponse.
  console.log("Created at:", page.created_time)
}

Please re-open this or open a new issue if there are other utilities you find are lacking here and thank you for the feedback.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Why Partial<Type> is an extremely useful TypeScript feature?
It constructs a type with all properties of Type set to optional. This utility will return a type that represents all subsets of...
Read more >
Documentation - Utility Types - TypeScript
Partial <Type>. Released: 2.1. Constructs a type with all properties of Type set to optional. This utility will return a type that represents...
Read more >
Why do Object.values for a Partial Record have undefined ...
1 Answer 1 · The Partial<T> utility type is implemented like type Partial<T> = {[K in keyof T]?: T[K]} and applies the optional...
Read more >
[@types/lodash] - FP typings: _.omit typings can be improved.
Running into this same issue. Omit's use of Partial is mucking up the types that come out of it. I was surprised to...
Read more >
Mastering TypeScript mapped types
Partial <Type> returns a type of the same shape as Type , but where all the keys are optional. An example where this...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found