Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
99e8167
[@types/lodash] Patches GetFieldType<> for paths with multiple square…
birdofpreyru May 28, 2024
f1f2162
[@types/lodash] Patch of GetIndexedField<>
birdofpreyru May 28, 2024
7c237b2
Patching/testing GetIndexedField<>
birdofpreyru May 29, 2024
97943a0
Reworked GetFieldType<>
birdofpreyru May 29, 2024
05c8720
Merge remote-tracking branch 'upstream/master' into lodash
birdofpreyru May 30, 2024
71d525a
Avoid underscores in function names
birdofpreyru May 30, 2024
24fe3d0
Yet another take on GetFieldType<>
birdofpreyru Jun 1, 2024
9002bfe
Merge remote-tracking branch 'upstream/master' into lodash
birdofpreyru Jun 1, 2024
246ac68
Minor optimization
birdofpreyru Jun 1, 2024
da1f854
Little fixes
birdofpreyru Jun 1, 2024
27279f8
Rework of tests for GetFieldTypes<> and related internal methods
birdofpreyru Jun 1, 2024
44504a6
Clean-up
birdofpreyru Jun 1, 2024
aac3887
Clean-up
birdofpreyru Jun 2, 2024
e34d541
Improvements for arrays & tuples
birdofpreyru Jun 2, 2024
1b93a33
Improves possibly undefined results when accessing object & string fi…
birdofpreyru Jun 2, 2024
b756938
Merge remote-tracking branch 'upstream/master' into lodash
birdofpreyru Jun 21, 2024
86b5b62
Merge remote-tracking branch 'upstream/master' into lodash
birdofpreyru Jun 22, 2024
83fa11f
Updates in response to the latest review
birdofpreyru Jun 22, 2024
e5a6912
Merge branch 'master' into lodash
birdofpreyru Jun 25, 2024
583ee7e
Merge remote-tracking branch 'upstream/master' into lodash
birdofpreyru Jun 25, 2024
e17c882
Minor correction
birdofpreyru Jun 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 89 additions & 32 deletions types/lodash/common/object.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1021,41 +1021,98 @@ declare module "../index" {
functionsIn(): CollectionChain<string>;
}

type GetIndexedField<T, K> = K extends keyof T
? T[K]
: K extends `${number}`
? 'length' extends keyof T
? number extends T['length']
? number extends keyof T
? T[number]
: undefined
: undefined
: undefined
: undefined;
type GetFieldTypeOfArrayLikeByKey<T extends unknown[], K> =
K extends number ? T[K]
: K extends `${infer N extends number}` ? T[N]
: K extends keyof T ? T[K] : undefined;

type FieldWithPossiblyUndefined<T, Key> =
| GetFieldType<Exclude<T, undefined>, Key>
| Extract<T, undefined>;
type GetFieldTypeOfStringByKey<T extends string, K> =
K extends number ? T[K]
: K extends `${infer N extends number}` ? T[N]
: K extends keyof T ? T[K]
: undefined;

type IndexedFieldWithPossiblyUndefined<T, Key> =
| GetIndexedField<Exclude<T, undefined>, Key>
| Extract<T, undefined>;
type GetFieldTypeOfNarrowedByKey<T, K> =
T extends unknown[] ? GetFieldTypeOfArrayLikeByKey<T, K>
: T extends string ? GetFieldTypeOfStringByKey<T, K>
: K extends keyof T ? T[K]
: K extends number
? `${K}` extends keyof T ? T[`${K}`] : undefined
: K extends `${infer N extends number}`
? N extends keyof T ? T[N] : undefined
: undefined;

type GetFieldType<T, P> = P extends `${infer Left}.${infer Right}`
? Left extends keyof Exclude<T, undefined>
? FieldWithPossiblyUndefined<Exclude<T, undefined>[Left], Right> | Extract<T, undefined>
: Left extends `${infer FieldKey}[${infer IndexKey}]`
? FieldKey extends keyof T
? FieldWithPossiblyUndefined<IndexedFieldWithPossiblyUndefined<T[FieldKey], IndexKey>, Right>
: undefined
: undefined
: P extends keyof T
? T[P]
: P extends `${infer FieldKey}[${infer IndexKey}]`
? FieldKey extends keyof T
? IndexedFieldWithPossiblyUndefined<T[FieldKey], IndexKey>
: undefined
: IndexedFieldWithPossiblyUndefined<T, P>;
/** Internal. Assumes P is a dot-delimited path. */
type GetFieldTypeOfNarrowedByDotPath<T, P> =
P extends `${infer L}.${infer R}`
? GetFieldType<GetFieldTypeOfNarrowedByKey<T, L>, R, 'DotPath'>
: GetFieldTypeOfNarrowedByKey<T, P>;

/** Internal. This is a piece of GetFieldTypeOfNarrowedByLKR logic,
* assuming that Lc isn't to be ignored, and does not end with dot. */
type GetFieldTypeOfNarrowedByLcKR<T, Lc, K, R> =
'' extends R
? GetFieldType<GetFieldTypeOfNarrowedByDotPath<T, Lc>, K, 'Key'>
: R extends `.${infer Rc}`
? GetFieldType<GetFieldType<GetFieldTypeOfNarrowedByDotPath<T, Lc>, K, 'Key'>, Rc>
: GetFieldType<GetFieldType<GetFieldTypeOfNarrowedByDotPath<T, Lc>, K, 'Key'>, R>

/** Internal. Assumes T has been narrowed; L is a dot-delimited path,
* and should be ignored if an empty string; K is a key name; and R is
* a dot-delimetered path, to be ignored if an empty string. Also if
* L has a tail dot, or R has a front dot, these dots should be discarded,
* however when L or R is just a dot, they should be interpreted as empty
* key name (rather than ignored). */
type GetFieldTypeOfNarrowedByLKR<T, L, K, R> =
'' extends L
? '' extends R
? GetFieldTypeOfNarrowedByKey<T, K>
: R extends `.${infer Rc}`
? GetFieldType<GetFieldTypeOfNarrowedByKey<T, K>, Rc>
: GetFieldType<GetFieldTypeOfNarrowedByKey<T, K>, R>
: L extends `${infer Lc}.`
? GetFieldTypeOfNarrowedByLcKR<T, Lc, K, R>
: GetFieldTypeOfNarrowedByLcKR<T, L, K, R>

/** Internal. Assumes T has been narrowed. */
type GetFieldTypeOfNarrowed<T, X, XT extends 'DotPath' | 'Key' | 'Path'> =
XT extends 'Key' ? GetFieldTypeOfNarrowedByKey<T, X>
: XT extends 'DotPath' ? GetFieldTypeOfNarrowedByDotPath<T, X>
: X extends `${infer L}['${infer K}']${infer R}`
? GetFieldTypeOfNarrowedByLKR<T, L, K, R>
: X extends `${infer L}["${infer K}"]${infer R}`
? GetFieldTypeOfNarrowedByLKR<T, L, K, R>
: X extends `${infer L}[${infer K}]${infer R}`
? GetFieldTypeOfNarrowedByLKR<T, L, K, R>
: GetFieldTypeOfNarrowedByDotPath<T, X>;

/** Internal. Assumes T has been narrowed to an object type. */
type GetFieldTypeOfObject<T, X, XT extends 'DotPath' | 'Key' | 'Path'> =
Extract<T, unknown[]> extends never
? GetFieldTypeOfNarrowed<T, X, XT>
: GetFieldTypeOfNarrowed<Exclude<T, unknown[]>, X, XT>
| GetFieldTypeOfNarrowed<Extract<T, unknown[]>, X, XT>;

/** Internal. Assumes T has been narrowed to a primitive type. */
type GetFieldTypeOfPrimitive<T, X, XT extends 'DotPath' | 'Key' | 'Path'> =
Extract<T, string> extends never
? T extends never ? never : undefined
: (Exclude<T, string> extends never ? never : undefined)
| GetFieldTypeOfNarrowed<Extract<T, string>, X, XT>;

/**
* Deduces the type of value at the path P of type T,
* so that _.get<T, P>(t: T, p: P): GetFieldType<T, P>.
* XT specifies the exact meaning of X:
* - 'Path' (default) - X is a path type to be fully parsed;
* - 'DotPath - X is a dot-delimitered path, without square (indexing) brackets;
* - 'Key' - X is a simple key, and needs no parsing.
*/
type GetFieldType<T, X, XT extends 'DotPath' | 'Key' | 'Path' = 'Path'> =
Extract<T, object> extends never
? GetFieldTypeOfPrimitive<T, X, XT>
: GetFieldTypeOfPrimitive<Exclude<T, object>, X, XT>
| GetFieldTypeOfObject<Extract<T, object>, X, XT>;

interface LoDashStatic {
/**
Expand Down
219 changes: 216 additions & 3 deletions types/lodash/lodash-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
import fp = require("./fp");
import _ = require("lodash");

import type {
GetFieldType,
GetFieldTypeOfNarrowed,
GetFieldTypeOfNarrowedByDotPath,
GetFieldTypeOfNarrowedByKey,
GetFieldTypeOfNarrowedByLKR,
GetFieldTypeOfObject,
GetFieldTypeOfPrimitive,
} from "lodash";

declare const anything: any;

interface AbcObject {
Expand Down Expand Up @@ -5355,6 +5365,14 @@ fp.now(); // $ExpectType number
const dictionary: _.Dictionary<string> = anything;
const maybeObject: { a: _.Dictionary<string> } | undefined = anything;

const complexValue: {
some: { test: { path: 'Value1' } };
'some.test.path': 'Value2';
'some[test]path': 'Value3';
'0': 'Value4';
'length': 'Value5';
} | number[] = anything;

_.get([], Symbol.iterator);
_.get([], [Symbol.iterator]);

Expand All @@ -5368,7 +5386,7 @@ fp.now(); // $ExpectType number
_.get({ a: tupleOfNumbers }, 'a.0'); // $ExpectType 1
_.get({ a: tupleOfNumbers }, 'a[0]'); // $ExpectType 1
_.get({ a: tupleOfNumbers }, 'a[1]'); // $ExpectType undefined
_.get({ a: tupleOfNumbers }, `a[${anyNumber}]`); // $ExpectType undefined
_.get({ a: tupleOfNumbers }, `a[${anyNumber}]`); // $ExpectType 1
_.get({ a: dictionary }, 'a.b'); // $ExpectType string
_.get(maybeObject, 'a.b'); // $ExpectType string | undefined
_.get("abc", [0], "_");
Expand All @@ -5385,6 +5403,19 @@ fp.now(); // $ExpectType number
_.get({}, "a", defaultValue); // $ExpectType boolean
_.get({}, "a" as string, defaultValue); // $ExpectType boolean
_.get(objectWithOptionalField, "a", undefined); // $ExpectType boolean | undefined
_.get(complexValue, 'some.test.path'); // $ExpectType 'Value1' | undefined
_.get(complexValue, '[some].test.path'); // $ExpectType 'Value1' | undefined
_.get(complexValue, 'some[test].path'); // $ExpectType 'Value1' | undefined
_.get(complexValue, 'some.test[path]'); // $ExpectType 'Value1' | undefined
_.get(complexValue, '[some][test].path'); // $ExpectType 'Value1' | undefined
_.get(complexValue, '[some].test[path]'); // $ExpectType 'Value1' | undefined
_.get(complexValue, 'some[test][path]'); // $ExpectType 'Value1' | undefined
_.get(complexValue, '[some][test][path]'); // $ExpectType 'Value1' | undefined
_.get(complexValue, '[some.test.path]'); // $ExpectType 'Value2' | undefined
_.get(complexValue, '["some[test]path"]'); // $ExpectType 'Value3' | undefined
_.get(complexValue, '0'); // $ExpectType 'Value4' | number
_.get(complexValue, '1'); // $ExpectType undefined | number
_.get(complexValue, 'length'); // $ExpectType 'Value5' | number

_("abc").get(1); // $ExpectType string
_({ a: false }).get("a"); // $ExpectType boolean
Expand All @@ -5396,7 +5427,7 @@ fp.now(); // $ExpectType number
_({ a: tupleOfNumbers }).get('a.0'); // $ExpectType 1
_({ a: tupleOfNumbers }).get('a[0]'); // $ExpectType 1
_({ a: tupleOfNumbers }).get('a[1]'); // $ExpectType undefined
_({ a: tupleOfNumbers }).get(`a[${anyNumber}]`); // $ExpectType undefined
_({ a: tupleOfNumbers }).get(`a[${anyNumber}]`); // $ExpectType 1
_({ a: dictionary }).get('a.b'); // $ExpectType string
_("abc").get([0], "_");
_([42]).get(0, -1); // $ExpectType number
Expand All @@ -5422,7 +5453,7 @@ fp.now(); // $ExpectType number
_.chain({ a: tupleOfNumbers }).get('a.0'); // $ExpectType PrimitiveChain<1>
_.chain({ a: tupleOfNumbers }).get('a[0]'); // $ExpectType PrimitiveChain<1>
_.chain({ a: tupleOfNumbers }).get('a[1]'); // $ExpectType never
_.chain({ a: tupleOfNumbers }).get(`a[${anyNumber}]`); // $ExpectType never
_.chain({ a: tupleOfNumbers }).get(`a[${anyNumber}]`); // $ExpectType PrimitiveChain<1>
_.chain({ a: dictionary }).get('a.b'); // $ExpectType StringChain
_.chain("abc").get([0], "_");
_.chain([42]).get(0, -1); // ExpectType PrimitiveChain<number>
Expand Down Expand Up @@ -7415,3 +7446,185 @@ _.templateSettings; // $ExpectType TemplateSettings
_.chain("").stubFalse(); // $ExpectType LoDashExplicitWrapper<false>
fp.stubFalse(); // $ExpectType false
}

/********************
* TypeScript Utils *
********************/

// GetFieldTypeOfNarrowedByKey
{
// of array
{
type Arr = Array<'OK'>;
type A = GetFieldTypeOfNarrowedByKey<Arr, 0>; // $ExpectType 'OK'
type B = GetFieldTypeOfNarrowedByKey<Arr, '0'>; // $ExpectType 'OK'
type C = GetFieldTypeOfNarrowedByKey<Arr, 'key'>; // $ExpectType undefined
type D = GetFieldTypeOfNarrowedByKey<Arr, 'length'>; // $ExpectType number
}

// of object
{
const symbol = Symbol();

interface ObjectLiteral { [key: string]: 'OK', [symbol]: 'SymVal' }
type ObjectWithNumberKeys = Record<number, 'OK'>; // keyof ObjectWithNumberKeys equals number
type ObjectWithStringKeys = Record<string, 'OK'>; // keyof ObjectWithStringKeys equals string

type A = GetFieldTypeOfNarrowedByKey<AbcObject, 'a'>; // $ExpectType number
type B = GetFieldTypeOfNarrowedByKey<AbcObject, 'b'>; // $ExpectType string
type C = GetFieldTypeOfNarrowedByKey<AbcObject, 'c'>; // $ExpectType boolean
type D = GetFieldTypeOfNarrowedByKey<AbcObject, 'd'>; // $ExpectType undefined
type E = GetFieldTypeOfNarrowedByKey<AbcObject, 0>; // $ExpectType undefined
type F = GetFieldTypeOfNarrowedByKey<AbcObject, '0'>; // $ExpectType undefined

type G = GetFieldTypeOfNarrowedByKey<ObjectLiteral, 'key'>; // $ExpectType 'OK'
type H = GetFieldTypeOfNarrowedByKey<ObjectLiteral, typeof symbol>; // $ExpectType "SymVal"

// Note: It is legit, as JS & TS auto-cast numeric values to string keys.
type I = GetFieldTypeOfNarrowedByKey<ObjectLiteral, 0>; // $ExpectType 'OK'

type J = GetFieldTypeOfNarrowedByKey<ObjectWithNumberKeys, 0>; // $ExpectType 'OK'
type K = GetFieldTypeOfNarrowedByKey<ObjectWithNumberKeys, '0'>; // $ExpectType 'OK'
type L = GetFieldTypeOfNarrowedByKey<ObjectWithNumberKeys, 'key'>; // $ExpectType undefined
type M = GetFieldTypeOfNarrowedByKey<ObjectWithNumberKeys, typeof symbol>; // $ExpectType undefined

// Again, JS & TS auto-cast numeric values to string keys, thus the next two test cases are valid.
type N = GetFieldTypeOfNarrowedByKey<ObjectWithStringKeys, 0>; // $ExpectType 'OK'
type O = GetFieldTypeOfNarrowedByKey<ObjectWithStringKeys, '0'>; // $ExpectType 'OK'
type P = GetFieldTypeOfNarrowedByKey<ObjectWithStringKeys, 'key'>; // $ExpectType 'OK'
type R = GetFieldTypeOfNarrowedByKey<ObjectWithStringKeys, typeof symbol>; // $ExpectType undefined
}

// of non-accessible primitives
{
const symbol = Symbol();
type A = GetFieldTypeOfNarrowedByKey<never, 'key'>; // $ExpectType never
type B = GetFieldTypeOfNarrowedByKey<null, 'key'>; // $ExpectType undefined
type C = GetFieldTypeOfNarrowedByKey<number, 'key'>; // $ExpectType undefined
type D = GetFieldTypeOfNarrowedByKey<typeof symbol, 'key'>; // $ExpectType undefined
type E = GetFieldTypeOfNarrowedByKey<undefined, 'key'>; // $ExpectType undefined
}

// of string
{
type A = GetFieldTypeOfNarrowedByKey<string, 0>; // $ExpectType string
type B = GetFieldTypeOfNarrowedByKey<string, '0'>; // $ExpectType string
type C = GetFieldTypeOfNarrowedByKey<string, 'key'>; // $ExpectType undefined
type D = GetFieldTypeOfNarrowedByKey<string, 'length'>; // $ExpectType number
}

// of tuple
{
type Tuple = ['A'];
type A = GetFieldTypeOfNarrowedByKey<Tuple, 0>; // $ExpectType 'A'
type B = GetFieldTypeOfNarrowedByKey<Tuple, 1>; // $ExpectType undefined
type C = GetFieldTypeOfNarrowedByKey<Tuple, number>; // $ExpectType 'A'
type D = GetFieldTypeOfNarrowedByKey<Tuple, '0'>; // $ExpectType 'A'
type E = GetFieldTypeOfNarrowedByKey<Tuple, '1'>; // $ExpectType undefined
type F = GetFieldTypeOfNarrowedByKey<Tuple, 'key'>; // $ExpectType undefined
type G = GetFieldTypeOfNarrowedByKey<Tuple, 'length'>; // $ExpectType 1
}
}

// GetFieldTypeOfNarrowedByDotPath
{
// handling of empty key names (based on the runtime _.get() behavior)
interface ObjA { '': { '': { '': 'OK' } } };
type A = GetFieldTypeOfNarrowedByDotPath<ObjA, ''>; // $ExpectType { '': { '': 'OK' } }
type B = GetFieldTypeOfNarrowedByDotPath<ObjA, '.'>; // $ExpectType { '': 'OK' }
type C = GetFieldTypeOfNarrowedByDotPath<ObjA, '..'>; // $ExpectType 'OK'
type D = GetFieldTypeOfNarrowedByDotPath<ObjA, '...'>; // $ExpectType undefined

// handling of objects with non-empty key names
interface ObjB { some: { path: 'OK' } };
type E = GetFieldTypeOfNarrowedByDotPath<ObjB, ''>; // $ExpectType undefined
type F = GetFieldTypeOfNarrowedByDotPath<ObjB, 'some'> // $ExpectType { path: 'OK' }
type G = GetFieldTypeOfNarrowedByDotPath<ObjB, 'some.path'> // $ExpectType 'OK'
type H = GetFieldTypeOfNarrowedByDotPath<ObjB, '.some'>; // $ExpectType undefined
}

// GetFieldTypeOfNarrowedByLKR
{
interface ObjA { some: { test: { path: 'OK' } } };

type A = GetFieldTypeOfNarrowedByLKR<ObjA, 'some.test', 'path', ''>; // $ExpectType 'OK'
type B = GetFieldTypeOfNarrowedByLKR<ObjA, 'some', 'test', 'path'>; // $ExpectType 'OK'
type C = GetFieldTypeOfNarrowedByLKR<ObjA, '', 'some', 'test.path'>; // $ExpectType 'OK'

// it should ignore the tail dot in L, and the front dot in R, if any
type D = GetFieldTypeOfNarrowedByLKR<ObjA, 'some.', 'test', '.path'>; // $ExpectType 'OK'
type E = GetFieldTypeOfNarrowedByLKR<ObjA, '.some', 'test', 'path.'>; // $ExpectType undefined

// and if there are empty key names, they should be handled correctly
type F = GetFieldTypeOfNarrowedByLKR<{ '': { test: 'OK' } }, '.', 'test', ''> // $ExpectType 'OK'
type G = GetFieldTypeOfNarrowedByLKR<{ test: { '': 'OK' } }, '', 'test', '.'> // $ExpectType 'OK'
type H = GetFieldTypeOfNarrowedByLKR<{ '': { test: { '': 'OK' } } }, '.', 'test', '.'> // $ExpectType 'OK'
}

// GetFieldTypeOfNarrowed
{
interface Obj {
some: { test: { path: 'Value1' } },
'some.test.path': 'Value2',
'some[test]path': 'Value3',
};

// XT argument causes expected path/key parsing mode
type A = GetFieldTypeOfNarrowed<Obj, 'some.test.path', 'Key'>; // $ExpectType 'Value2'
type B = GetFieldTypeOfNarrowed<Obj, 'some[test]path', 'Key'>; // $ExpectType 'Value3'
type C = GetFieldTypeOfNarrowed<Obj, 'some.test.path', 'DotPath'>; // $ExpectType 'Value1'
type D = GetFieldTypeOfNarrowed<Obj, 'some[test]path', 'DotPath'>; // $ExpectType 'Value3'
type E = GetFieldTypeOfNarrowed<Obj, 'some.test.path', 'Path'>; // $ExpectType 'Value1'
type F = GetFieldTypeOfNarrowed<Obj, 'some[test]path', 'Path'>; // $ExpectType 'Value1'

// Dots around brackets are handled the same as by _.get() during the runtime
type G = GetFieldTypeOfNarrowed<Obj, '.some[test]path', 'Path'>; // $ExpectType undefined
type H = GetFieldTypeOfNarrowed<Obj, 'some.[test]path', 'Path'>; // $ExpectType 'Value1'
type I = GetFieldTypeOfNarrowed<Obj, 'some[test].path', 'Path'>; // $ExpectType 'Value1'
type J = GetFieldTypeOfNarrowed<Obj, 'some[test]path.', 'Path'>; // $ExpectType undefined

// Brackets in front, or in the end
type K = GetFieldTypeOfNarrowed<Obj, '[some]test.path', 'Path'>; // $ExpectType 'Value1'
type L = GetFieldTypeOfNarrowed<Obj, '.[some]test.path', 'Path'>; // $ExpectType undefined
type M = GetFieldTypeOfNarrowed<Obj, 'some.test[path]', 'Path'>; // $ExpectType 'Value1'
type N = GetFieldTypeOfNarrowed<Obj, 'some.test[path].', 'Path'>; // $ExpectType undefined

// Key inside brackets can be quoted, to avoid parsing any dots / brackets inside it
type O = GetFieldTypeOfNarrowed<Obj, "['some.test.path']", 'Path'>; // $ExpectType 'Value2'
type P = GetFieldTypeOfNarrowed<Obj, '["some[test]path"]', 'Path'>; // $ExpectType 'Value3'
}

// GetFieldTypeOfObject
{
type Arr = Array<'ArrVal'>;
interface Obj { '0': 'ObjVal' };
type ArrOrObj = Arr | Obj;

type A = GetFieldTypeOfObject<Arr, 0, 'Path'>; // $ExpectType 'ArrVal'
type B = GetFieldTypeOfObject<Obj, 0, 'Path'>; // $ExpectType 'ObjVal'
type C = GetFieldTypeOfObject<ArrOrObj, 0, 'Path'>; // $ExpectType 'ArrVal' | 'ObjVal'
type D = GetFieldTypeOfObject<ArrOrObj, 'length', 'Path'>; // $ExpectType number | undefined
}

// GetFieldTypeOfPrimitive
{
type A = GetFieldTypeOfPrimitive<string, 0, 'Path'>; // $ExpectType string
type B = GetFieldTypeOfPrimitive<null, 0, 'Path'>; // $ExpectType undefined
type C = GetFieldTypeOfPrimitive<never, 0, 'Path'>; // $ExpectType never
type D = GetFieldTypeOfPrimitive<string | null, 0, 'Path'>; // $ExpectType string | undefined
type E = GetFieldTypeOfPrimitive<string | null, 'length', 'Path'>; // $ExpectType number | undefined
}

// GetFieldType
{
type Type =
| Array<'ArrVal'>
| { '0': 'ObjVal0', 'A': 'ObjValA' }
| string
| undefined;

type A = GetFieldType<Type, 0>; // $ExpectType string | undefined
type B = GetFieldType<Exclude<Type, string>, 0>; // $ExpectType 'ArrVal' | 'ObjVal0' | undefined
type C = GetFieldType<Type, 'A'>; // $ExpectType 'ObjValA' | undefined
type D = GetFieldType<Type, 'length'>; // $ExpectType number | undefined
}