Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure MaybeMasked doesn't unwrap if it contains any #12204

Merged
merged 11 commits into from
Dec 12, 2024
5 changes: 5 additions & 0 deletions .changeset/friendly-papayas-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

Fix `Unmasked` unwrapping tuple types into an array of their subtypes.
5 changes: 5 additions & 0 deletions .changeset/happy-weeks-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

Ensure `MaybeMasked` does not try and unwrap types that contain index signatures.
5 changes: 5 additions & 0 deletions .changeset/three-pandas-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

Ensure `MaybeMasked` does not try to unwrap the type as `Unmasked` if the type contains `any`.
115 changes: 114 additions & 1 deletion src/masking/__benches__/types.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ test("MaybeMasked handles odd types", (prefix) => {

bench(prefix + "unknown instantiations", () => {
attest<unknown, MaybeMasked<unknown>>();
}).types([52, "instantiations"]);
}).types([54, "instantiations"]);
bench(prefix + "unknown functionality", () => {
expectTypeOf<MaybeMasked<unknown>>().toBeUnknown();
});
Expand Down Expand Up @@ -464,3 +464,116 @@ test("base type, multiple fragments on sub-types", (prefix) => {
}>();
});
});

test("does not detect `$fragmentRefs` if type contains `any`", (prefix) => {
interface Source {
foo: { bar: any[] };
" $fragmentName": "foo";
}

bench(prefix + "instantiations", () => {
return {} as MaybeMasked<Source>;
}).types([6, "instantiations"]);

bench(prefix + "functionality", () => {
const x = {} as MaybeMasked<Source>;

expectTypeOf(x).branded.toEqualTypeOf<Source>();
});
});

test("leaves tuples alone", (prefix) => {
interface Source {
coords: [long: number, lat: number];
}

bench(prefix + "instantiations", () => {
return {} as Unmasked<Source>;
}).types([5, "instantiations"]);

bench(prefix + "functionality", () => {
const x = {} as Unmasked<Source>;

expectTypeOf(x).branded.toEqualTypeOf<{
coords: [long: number, lat: number];
}>();
});
});

test("does not detect `$fragmentRefs` if type is a record type", (prefix) => {
interface MetadataItem {
foo: string;
}

interface Source {
metadata: Record<string, MetadataItem>;
" $fragmentName": "Source";
}

bench(prefix + "instantiations", () => {
return {} as MaybeMasked<Source>;
}).types([6, "instantiations"]);

bench(prefix + "functionality", () => {
const x = {} as MaybeMasked<Source>;

expectTypeOf(x).branded.toEqualTypeOf<Source>();
});
});

test("does not detect `$fragmentRefs` on types with index signatures", (prefix) => {
interface Source {
foo: string;
" $fragmentName": "Source";
[key: string]: string;
}

bench(prefix + "instantiations", () => {
return {} as MaybeMasked<Source>;
}).types([6, "instantiations"]);

bench(prefix + "functionality", () => {
const x = {} as MaybeMasked<Source>;

expectTypeOf(x).branded.toEqualTypeOf<Source>();
});
});

test("detects `$fragmentRefs` on types with index signatures", (prefix) => {
type Source = {
__typename: "Foo";
id: number;
metadata: Record<string, number>;
structuredMetadata: StructuredMetadata;
} & { " $fragmentName"?: "UserFieldsFragment" } & {
" $fragmentRefs"?: {
FooFragment: FooFragment;
};
};

interface StructuredMetadata {
bar: number;
[index: string]: number;
}

type FooFragment = {
__typename: "Foo";
foo: string;
} & { " $fragmentName"?: "FooFragment" };

bench(prefix + "instantiations", () => {
return {} as MaybeMasked<Source>;
}).types([6, "instantiations"]);

bench(prefix + "functionality", () => {
const x = {} as MaybeMasked<Source>;

expectTypeOf(x).branded.toEqualTypeOf<{
__typename: "Foo";
id: number;
metadata: Record<string, number>;
foo: string;
structuredMetadata: StructuredMetadata;
}>();
});
});
8 changes: 4 additions & 4 deletions src/masking/internal/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Prettify } from "../../utilities/index.js";
import type { Prettify, RemoveIndexSignature } from "../../utilities/index.js";

export type IsAny<T> = 0 extends 1 & T ? true : false;

Expand All @@ -20,7 +20,6 @@ export type UnwrapFragmentRefs<TData> =
>
>
>
: TData extends Array<infer TItem> ? Array<UnwrapFragmentRefs<TItem>>
: TData extends object ?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

{
[K in keyof TData]: UnwrapFragmentRefs<TData[K]>;
Expand Down Expand Up @@ -184,8 +183,9 @@ export type RemoveFragmentName<T> =
T extends any ? Omit<T, " $fragmentName"> : T;

export type ContainsFragmentsRefs<TData> =
TData extends object ?
" $fragmentRefs" extends keyof TData ?
true extends IsAny<TData> ? false
: TData extends object ?
" $fragmentRefs" extends keyof RemoveIndexSignature<TData> ?
true
: ContainsFragmentsRefs<TData[keyof TData]>
: false;
1 change: 1 addition & 0 deletions src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export type { OnlyRequiredProperties } from "./types/OnlyRequiredProperties.js";
export type { Prettify } from "./types/Prettify.js";
export type { UnionToIntersection } from "./types/UnionToIntersection.js";
export type { NoInfer } from "./types/NoInfer.js";
export type { RemoveIndexSignature } from "./types/RemoveIndexSignature.js";

export {
AutoCleanedStrongCache,
Expand Down
6 changes: 6 additions & 0 deletions src/utilities/types/RemoveIndexSignature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type RemoveIndexSignature<T> = {
[K in keyof T as string extends K ? never
: number extends K ? never
: symbol extends K ? never
: K]: T[K];
};
Loading