Skip to content

Commit

Permalink
🛤️ keep track of traversed types to avoid self-referencing while cons…
Browse files Browse the repository at this point in the history
…tructing paths for a type (#9540)

* Keep track of traversed types to avoid self-referencing while constructing  paths for a type

* Ran api-extractor

* Addressed code review

* Impoved robustness of recursion avoidance and added tests

Co-authored-by: Beier (Bill) <bluebill1049@hotmail.com>
  • Loading branch information
SimplyLinn and bluebill1049 committed Dec 16, 2022
1 parent 411c8c1 commit 52f5f95
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 41 deletions.
24 changes: 9 additions & 15 deletions reports/api-extractor.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,10 @@ import { ReactElement } from 'react';
// @public (undocumented)
export const appendErrors: (name: InternalFieldName, validateAllFieldCriteria: boolean, errors: InternalFieldErrors, type: string, message: ValidateResult) => {};

// Warning: (ae-forgotten-export) The symbol "IsTuple" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "TupleKeys" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "ArrayPathImpl" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "ArrayKey" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "ArrayPathInternal" needs to be exported by the entry point index.d.ts
//
// @public
export type ArrayPath<T> = T extends ReadonlyArray<infer V> ? IsTuple<T> extends true ? {
[K in TupleKeys<T>]-?: ArrayPathImpl<K & string, T[K]>;
}[TupleKeys<T>] : ArrayPathImpl<ArrayKey, V> : {
[K in keyof T]-?: ArrayPathImpl<K & string, T[K]>;
}[keyof T];
export type ArrayPath<T> = T extends any ? ArrayPathInternal<T> : never;

// @public (undocumented)
export type BatchFieldArrayUpdate = <T extends Function, TFieldValues extends FieldValues, TFieldArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>>(name: InternalFieldName, updatedFieldArrayValues?: Partial<FieldArray<TFieldValues, TFieldArrayName>>[], method?: T, args?: Partial<{
Expand Down Expand Up @@ -300,6 +293,9 @@ export type InternalNameSet = Set<InternalFieldName>;
// @public
export type IsAny<T> = 0 extends 1 & T ? true : false;

// @public
export type IsEqual<T1, T2> = T1 extends T2 ? (<G>() => G extends T1 ? 1 : 2) extends <G>() => G extends T2 ? 1 : 2 ? true : false : false;

// @public (undocumented)
export type IsFlatObject<T extends object> = Extract<Exclude<T[keyof T], NestedValue | Date | FileList_2>, any[] | object> extends never ? true : false;

Expand Down Expand Up @@ -366,18 +362,16 @@ export type NonUndefined<T> = T extends undefined ? never : T;
// @public (undocumented)
export type Noop = () => void;

// Warning: (ae-forgotten-export) The symbol "PathImpl" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "PathInternal" needs to be exported by the entry point index.d.ts
//
// @public
export type Path<T> = T extends ReadonlyArray<infer V> ? IsTuple<T> extends true ? {
[K in TupleKeys<T>]-?: PathImpl<K & string, T[K]>;
}[TupleKeys<T>] : PathImpl<ArrayKey, V> : {
[K in keyof T]-?: PathImpl<K & string, T[K]>;
}[keyof T];
export type Path<T> = T extends any ? PathInternal<T> : never;

// @public
export type PathString = string;

// Warning: (ae-forgotten-export) The symbol "ArrayKey" needs to be exported by the entry point index.d.ts
//
// @public
export type PathValue<T, P extends Path<T> | ArrayPath<T>> = T extends any ? P extends `${infer K}.${infer R}` ? K extends keyof T ? R extends Path<T[K]> ? PathValue<T[K], R> : never : K extends `${ArrayKey}` ? T extends ReadonlyArray<infer V> ? PathValue<V, R & Path<V>> : never : never : P extends keyof T ? T[P] : P extends `${ArrayKey}` ? T extends ReadonlyArray<infer V> ? V : never : never : never;

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/type.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ test('should not throw type error with optional array fields', () => {

{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`things.${index}.name`)} />
<input {...register(`things.${index}.name` as const)} />
</div>
))}
{fieldArray.fields.map((item) => {
Expand Down
38 changes: 38 additions & 0 deletions src/__typetest__/path/eager.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ import { _, Depth3Type } from '../__fixtures__';
const actual = _ as Path<{ foo: string[] }>;
expectType<'foo' | `foo.${number}`>(actual);
}

/** it should be able to avoid self-referencing/recursion, not crashing on self-referencing types. */ {
type Foo = { foo: Foo };
const actual = _ as Path<Foo>;
expectType<'foo'>(actual);
}

/** it should not erroneously match subtypes as traversed */ {
type Foo =
| {
foo?: Foo;
bar?: {
baz: 1;
};
}
| {};
const actual = _ as Path<Foo>;
expectType<'foo' | 'bar' | 'bar.baz'>(actual);
}
}

/** {@link ArrayPath} */ {
Expand All @@ -42,6 +61,25 @@ import { _, Depth3Type } from '../__fixtures__';
const actual = _ as ArrayPath<{ foo: string[][][] }>;
expectType<'foo' | `foo.${number}`>(actual);
}

/** it should be able to avoid self-referencing/recursion, not crashing on self-referencing types. */ {
type Foo = { foo: Foo[] };
const actual = _ as ArrayPath<Foo>;
expectType<'foo'>(actual);
}

/** it should not erroneously match subtypes as traversed */ {
type Foo =
| {
bar?: {
baz?: 1;
fooArr?: Foo[];
};
}
| {};
const actual = _ as ArrayPath<Foo>;
expectType<'bar.fooArr'>(actual);
}
}

/** {@link PathValue} */ {
Expand Down
105 changes: 80 additions & 25 deletions src/types/path/eager.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,58 @@
import { FieldValues } from '../fields';
import {
BrowserNativeObject,
IsAny,
IsEqual,
Primitive,
UnPackAsyncDefaultValues,
} from '../utils';

import { ArrayKey, IsTuple, TupleKeys } from './common';

/**
* Helper function to break apart T1 and check if any are equal to T2
*
* See {@link IsEqual}
*/
type AnyIsEqual<T1, T2> = T1 extends T2
? IsEqual<T1, T2> extends true
? true
: never
: never;

/**
* Helper type for recursively constructing paths through a type.
* This actually constructs the strings and recurses into nested
* object types.
*
* See {@link Path}
*/
type PathImpl<K extends string | number, V> = V extends
type PathImpl<K extends string | number, V, TraversedTypes> = V extends
| Primitive
| BrowserNativeObject
? `${K}`
: `${K}` | `${K}.${Path<V>}`;
: // Check so that we don't recurse into the same type
// by ensuring that the types are mutually assignable
// mutually required to avoid false positives of subtypes
true extends AnyIsEqual<TraversedTypes, V>
? `${K}`
: `${K}` | `${K}.${PathInternal<V, TraversedTypes | V>}`;

/**
* Helper type for recursively constructing paths through a type.
* This obsucres the internal type param TraversedTypes from exported contract.
*
* See {@link Path}
*/
type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes>;
}[TupleKeys<T>]
: PathImpl<ArrayKey, V, TraversedTypes>
: {
[K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes>;
}[keyof T];

/**
* Type which eagerly collects all paths through a type
Expand All @@ -25,15 +62,9 @@ type PathImpl<K extends string | number, V> = V extends
* Path<{foo: {bar: string}}> = 'foo' | 'foo.bar'
* ```
*/
export type Path<T> = T extends ReadonlyArray<infer V>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: PathImpl<K & string, T[K]>;
}[TupleKeys<T>]
: PathImpl<ArrayKey, V>
: {
[K in keyof T]-?: PathImpl<K & string, T[K]>;
}[keyof T];
// We want to explode the union type and process each individually
// so assignable types don't leak onto the stack from the base.
export type Path<T> = T extends any ? PathInternal<T> : never;

/**
* See {@link Path}
Expand All @@ -44,36 +75,60 @@ export type FieldPath<TFieldValues extends FieldValues> = Path<

/**
* Helper type for recursively constructing paths through a type.
* This actually constructs the strings and recurses into nested
* object types.
*
* See {@link ArrayPath}
*/
type ArrayPathImpl<K extends string | number, V> = V extends
type ArrayPathImpl<K extends string | number, V, TraversedTypes> = V extends
| Primitive
| BrowserNativeObject
? never
? IsAny<V> extends true
? string
: never
: V extends ReadonlyArray<infer U>
? U extends Primitive | BrowserNativeObject
? IsAny<V> extends true
? string
: never
: // Check so that we don't recurse into the same type
// by ensuring that the types are mutually assignable
// mutually required to avoid false positives of subtypes
true extends AnyIsEqual<TraversedTypes, V>
? never
: `${K}` | `${K}.${ArrayPath<V>}`
: `${K}.${ArrayPath<V>}`;
: `${K}` | `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`
: true extends AnyIsEqual<TraversedTypes, V>
? never
: `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`;

/**
* Helper type for recursively constructing paths through a type.
* This obsucres the internal type param TraversedTypes from exported contract.
*
* See {@link ArrayPath}
*/
type ArrayPathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: ArrayPathImpl<K & string, T[K], TraversedTypes>;
}[TupleKeys<T>]
: ArrayPathImpl<ArrayKey, V, TraversedTypes>
: {
[K in keyof T]-?: ArrayPathImpl<K & string, T[K], TraversedTypes>;
}[keyof T];

/**
* Type which eagerly collects all paths through a type which point to an array
* type.
* @typeParam T - type which should be introspected
* @typeParam T - type which should be introspected.
* @example
* ```
* Path<{foo: {bar: string[], baz: number[]}}> = 'foo.bar' | 'foo.baz'
* ```
*/
export type ArrayPath<T> = T extends ReadonlyArray<infer V>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: ArrayPathImpl<K & string, T[K]>;
}[TupleKeys<T>]
: ArrayPathImpl<ArrayKey, V>
: {
[K in keyof T]-?: ArrayPathImpl<K & string, T[K]>;
}[keyof T];
// We want to explode the union type and process each individually
// so assignable types don't leak onto the stack from the base.
export type ArrayPath<T> = T extends any ? ArrayPathInternal<T> : never;

/**
* See {@link ArrayPath}
Expand Down
20 changes: 20 additions & 0 deletions src/types/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,26 @@ export type IsAny<T> = 0 extends 1 & T ? true : false;
*/
export type IsNever<T> = [T] extends [never] ? true : false;

/**
* Checks whether T1 can be exactly (mutually) assigned to T2
* @typeParam T1 - type to check
* @typeParam T2 - type to check against
* ```
* IsEqual<string, string> = true
* IsEqual<'foo', 'foo'> = true
* IsEqual<string, number> = false
* IsEqual<string, number> = false
* IsEqual<string, 'foo'> = false
* IsEqual<'foo', string> = false
* IsEqual<'foo' | 'bar', 'foo'> = boolean // 'foo' is assignable, but 'bar' is not (true | false) -> boolean
* ```
*/
export type IsEqual<T1, T2> = T1 extends T2
? (<G>() => G extends T1 ? 1 : 2) extends <G>() => G extends T2 ? 1 : 2
? true
: false
: false;

export type DeepMap<T, TValue> = IsAny<T> extends true
? any
: T extends BrowserNativeObject | NestedValue
Expand Down

0 comments on commit 52f5f95

Please sign in to comment.