refactor(typescript-types): Refine KeyPaths<T,O,F> logic

This commit is contained in:
T. R. Bernstein
2025-07-17 23:46:04 +02:00
parent c3238a6847
commit 5461fc52b4
2 changed files with 36 additions and 27 deletions

View File

@@ -1,5 +1,7 @@
import type { If } from './if.js' import type { If } from './if.js'
import type { IsEmptyString } from './is-empty-string.js' import type { IsEmptyString } from './is-empty-string.js'
import type { IsUndefined } from './is-undefined.js'
import type { Simplify } from './simplify.js'
import type { Assign } from './assign.js' import type { Assign } from './assign.js'
interface KeyPaths_Options { interface KeyPaths_Options {
@@ -20,15 +22,10 @@ type GetPrefixedKey<
type PrefixIfNot<Cond, Prefix, T> = If<Cond, T, Prefix | T> type PrefixIfNot<Cond, Prefix, T> = If<Cond, T, Prefix | T>
type KeyPathOf< type KeyPathOf<Obj, Options extends Required<KeyPaths_Options>, Parent extends string, Filter = never> =
Obj, Obj extends Record<PropertyKey, unknown>
Key extends PropertyKey, ? PrefixIfNot<Options['leavesOnly'], Parent, KeyPathsOfStringKeys<Obj, Options, Filter, Parent>>
Options extends Required<KeyPaths_Options>, : If<IsUndefined<Obj>, never, Parent>
Parent extends string,
Filter = never
> = Obj extends { [P in Key]: Record<PropertyKey, unknown> }
? PrefixIfNot<Options['leavesOnly'], Parent, KeyPathsOfStringKeys<Obj[Key], Options, Filter, Parent>>
: Parent
type KeyPathsOfStringKeys< type KeyPathsOfStringKeys<
Obj extends object, Obj extends object,
@@ -36,13 +33,13 @@ type KeyPathsOfStringKeys<
Filter, Filter,
Parent extends string = '' Parent extends string = ''
> = { > = {
[Key in keyof Obj & string]: [Filter] extends [Exclude<Filter, Obj[Key]>] [Key in keyof Obj & string]: Obj[Key] extends Filter
? KeyPathOf<Obj, Key, Options, GetPrefixedKey<Parent, Key, Options['separator']>, Filter> ? never
: never : KeyPathOf<Obj[Key], Options, GetPrefixedKey<Parent, Key, Options['separator']>, Filter>
}[keyof Obj & string] }[keyof Obj & string]
export type KeyPaths< export type KeyPaths<
Obj extends object, Obj extends object,
Options extends KeyPaths_Options = {}, Options extends KeyPaths_Options = {},
Filter = null | undefined Filter = null | undefined
> = KeyPathsOfStringKeys<Obj, Assign<KeyPaths_Options, KeyPaths_DefaultOptions, Options>, Filter> > = Simplify<KeyPathsOfStringKeys<Obj, Assign<KeyPaths_Options, KeyPaths_DefaultOptions, Options>, Filter>>

View File

@@ -4,7 +4,10 @@ import { expect } from 'tstyche'
interface ExampleObject { interface ExampleObject {
nullvalue: null nullvalue: null
simplevalue: string simplevalue: string
optionalKey?: string optionalKey?: {
nullvalue: null
simplevalue: string
}
unknownKey: unknown unknownKey: unknown
neverKey: never neverKey: never
config: { config: {
@@ -13,27 +16,36 @@ interface ExampleObject {
} }
} }
expect<KeyPaths<ExampleObject>>().type.toBe<'simplevalue' | 'config.simplevalue'>() expect<KeyPaths<ExampleObject>>().type.toBe<
expect<KeyPaths<ExampleObject, {}, any>>().type.toBeAssignableTo< 'simplevalue' | 'optionalKey.simplevalue' | 'unknownKey' | 'config.simplevalue'
'nullvalue' | 'simplevalue' | 'optionalKey' | 'config.nullvalue' | 'config.simplevalue'
>()
expect<KeyPaths<ExampleObject, {}, unknown>>().type.toBeAssignableTo<
'nullvalue' | 'simplevalue' | 'optionalKey' | 'config.nullvalue' | 'config.simplevalue'
>() >()
expect<KeyPaths<ExampleObject, {}, any>>().type.toBeAssignableTo<never>()
expect<KeyPaths<ExampleObject, {}, unknown>>().type.toBeAssignableTo<never>()
expect<KeyPaths<ExampleObject, {}, never>>().type.toBe< expect<KeyPaths<ExampleObject, {}, never>>().type.toBe<
'nullvalue' | 'simplevalue' | 'optionalKey' | 'unknownKey' | 'config.nullvalue' | 'config.simplevalue' | 'nullvalue'
| 'simplevalue'
| 'optionalKey.nullvalue'
| 'optionalKey.simplevalue'
| 'unknownKey'
| 'config.nullvalue'
| 'config.simplevalue'
>()
expect<KeyPaths<ExampleObject, {}, null>>().type.toBe<
'simplevalue' | 'optionalKey.simplevalue' | 'unknownKey' | 'config.simplevalue'
>()
expect<KeyPaths<ExampleObject, {}, string>>().type.toBe<
'nullvalue' | 'optionalKey.nullvalue' | 'unknownKey' | 'config.nullvalue'
>()
expect<KeyPaths<ExampleObject, { separator: '-' }>>().type.toBe<
'simplevalue' | 'optionalKey-simplevalue' | 'unknownKey' | 'config-simplevalue'
>() >()
expect<KeyPaths<ExampleObject, {}, null>>().type.toBe<'simplevalue' | 'optionalKey' | 'config.simplevalue'>()
expect<KeyPaths<ExampleObject, {}, string>>().type.toBe<'nullvalue' | 'config.nullvalue'>()
expect<KeyPaths<ExampleObject, { separator: '-' }>>().type.toBe<'simplevalue' | 'config-simplevalue'>()
expect<KeyPaths<ExampleObject, { leavesOnly: false }>>().type.toBe< expect<KeyPaths<ExampleObject, { leavesOnly: false }>>().type.toBe<
'simplevalue' | 'config' | 'neverKey' | 'config.simplevalue' 'simplevalue' | 'optionalKey' | 'optionalKey.simplevalue' | 'unknownKey' | 'config' | 'config.simplevalue'
>() >()
expect<KeyPaths<any, {}, string>>().type.toBeAssignableTo<never>()
expect<KeyPaths<any, {}, any>>().type.toBeAssignableTo<never>() expect<KeyPaths<any, {}, any>>().type.toBeAssignableTo<never>()
expect<KeyPaths<never, {}, any>>().type.toBeAssignableTo<never>() expect<KeyPaths<never, {}, any>>().type.toBeAssignableTo<never>()
expect<KeyPaths<never, {}, never>>().type.toBeAssignableTo<never>() expect<KeyPaths<never, {}, never>>().type.toBeAssignableTo<never>()
expect<KeyPaths<any, {}, never>>().type.toBe<unknown>() expect<KeyPaths<any, {}, never>>().type.toBe<unknown>()
expect<KeyPaths<any, {}, string>>().type.toBeAssignableTo<unknown>()