1

I hope my title is clear but I'm relatively new to TS and I'm struggling with union types.

I am trying to make a generic callApi function which allows optional paging and filters query parameters. However, I want to make the filter options strongly typed, so for example, when calling /api/people/, I only want to allow PeopleFilterQueryParams.

This is what I've come up with until now, however, it is allowing both the id and the after filter query parameter - when it should only allow one of the two.

export type BookingQueryParams = {
    before?: string;
    after?: string;
}

export type PersonQueryParams = {
    id?: number;
}

export type FilterQueryParams = BookingQueryParams | PersonQueryParams;

export interface PaginationQueryParams {
    page?: number;
    size?: number;
}

export interface ApiRequestParams {
    filters?: FilterQueryParams;
    paging?: PaginationQueryParams;
}

export const callApi = (url: string, params?: ApiRequestParams) => {
    return ``;
}

callApi('/people', {
    filters: {
        id: 123,
        after: '',
    }
})

I have a TS playground link here demonstrating my problem: TS playground

Any help would be greatly appreciated!

Olli
  • 658
  • 5
  • 26
nbokmans
  • 5,492
  • 4
  • 35
  • 59

1 Answers1

0

I believe that's because of some bug with nested unions. Can't find the github issue.

However, there is a workaround.

export type BookingQueryParams = {
    before?: string;
    after?: string;
}

export type PersonQueryParams = {
    id?: number;
}

type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> = 
    T extends any 
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

// credits goes to https://stackoverflow.com/questions/65805600/struggling-with-building-a-type-in-ts#answer-65805753
type StrictUnion<T> = StrictUnionHelper<T, T>

export type FilterQueryParams = StrictUnion<BookingQueryParams | PersonQueryParams>;

export interface PaginationQueryParams {
    page?: number;
    size?: number;
}

export interface ApiRequestParams {
    filters?: FilterQueryParams;
    paging?: PaginationQueryParams;
}

export const callApi = (url: string, params?: ApiRequestParams) => {
    return ``;
}

callApi('/people', {
    filters: {
        id: 123,
        after: '', // error
    }
})

Credits goes to Titian Cernicova-Dragomir

You just need to make union much more stricted