From b8043d17b0fc8bdb5e7728d70b2d451d05b2718e Mon Sep 17 00:00:00 2001 From: ben-tilden Date: Sat, 4 Jan 2025 17:32:03 -0600 Subject: [PATCH 1/2] fix(types): fix ExtractedValue type TypeScript does not automatically distribute conditionals unless the generic type itself represents a union. So, in `ExtractedValue`, `V['value']` is not distributed unless inferred as a separate generic type. ``` // Testing distributed conditional over an indexed access type interface Obj { value: string | number; } type TestIndexedObj = V['value'] extends number ? 'extends number' : 'does not extend number'; type TestIndexedObjResult = TestIndexedObj // "does not extend number" // Testing distributed conditional over a union type ObjValue = Obj['value']; type TestUnion = V extends number ? 'extends number' : 'does not extend number'; type TestUnionResult = TestUnion // "does not extend number" | "extends number" // Testing distributed conditional over an inferred generic from an indexed access type type TestInferredIndexedObj = V['value'] extends infer U ? (U extends number ? 'extends number' : 'does not extend number') : never; type TestInferredIndexedObjResult = TestInferredIndexedObj // "does not extend number" | "extends number" ``` --- src/api/extract.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/api/extract.ts b/src/api/extract.ts index 8bb0b62cb6..238c865df9 100644 --- a/src/api/extract.ts +++ b/src/api/extract.ts @@ -18,22 +18,24 @@ type ExtractValue = string | ExtractDescriptor | [string | ExtractDescriptor]; export type ExtractMap = Record; -type ExtractedValue = V extends [ +type ExtractedValue = V extends [ string | ExtractDescriptor, ] - ? NonNullable>[] + ? NonNullable>[] : V extends string ? string | undefined : V extends ExtractDescriptor - ? V['value'] extends ExtractMap - ? ExtractedMap | undefined - : V['value'] extends ExtractDescriptorFn - ? ReturnType | undefined - : ReturnType | undefined + ? V['value'] extends infer U + ? U extends ExtractMap + ? ExtractedMap | undefined + : U extends ExtractDescriptorFn + ? ReturnType | undefined + : ReturnType | undefined + : never : never; export type ExtractedMap = { - [key in keyof M]: ExtractedValue; + [key in keyof M]: ExtractedValue; }; function getExtractDescr( From 5c4f05abd5917e967713f4ae3ffb3cf831849218 Mon Sep 17 00:00:00 2001 From: ben-tilden Date: Tue, 18 Feb 2025 20:55:29 -0500 Subject: [PATCH 2/2] test: test ExtractedValue typing --- src/api/extract.spec.ts | 60 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/api/extract.spec.ts b/src/api/extract.spec.ts index a8a485763c..47ee3b1437 100644 --- a/src/api/extract.spec.ts +++ b/src/api/extract.spec.ts @@ -152,6 +152,28 @@ describe('$.extract', () => { ).toStrictEqual({ red: 'red=Four' }); }); + it('should correctly type check custom extraction functions returning non-string values', () => { + const $ = load(fixtures.eleven); + const $root = $.root(); + + expectTypeOf( + $root.extract({ + red: { + selector: '.red', + value: (el) => $(el).text().length, + }, + }), + ).toEqualTypeOf<{ red: number | undefined }>(); + expect( + $root.extract({ + red: { + selector: '.red', + value: (el) => $(el).text().length, + }, + }), + ).toStrictEqual({ red: 4 }); + }); + it('should extract multiple values using custom extraction functions', () => { const $ = load(fixtures.eleven); const $root = $.root(); @@ -215,6 +237,44 @@ describe('$.extract', () => { }); }); + it('should correctly type check nested objects returning non-string values', () => { + const $ = load(fixtures.eleven); + const $root = $.root(); + + expectTypeOf( + $root.extract({ + section: { + selector: 'ul:nth(1)', + value: { + red: { + selector: '.red', + value: (el) => $(el).text().length, + }, + }, + }, + }), + ).toEqualTypeOf<{ + section: { red: number | undefined } | undefined; + }>(); + expect( + $root.extract({ + section: { + selector: 'ul:nth(1)', + value: { + red: { + selector: '.red', + value: (el) => $(el).text().length, + }, + }, + }, + }), + ).toStrictEqual({ + section: { + red: 4, + }, + }); + }); + it('should handle missing href properties without errors (#4239)', () => { const $ = load(fixtures.eleven); expect<{ links: string[] }>(