I have a type definition that allows me to navigate through an object via an array of strings / indices representing the keys of the object or nested arrays:
export type PredicateFunction<ArrayType> = (array: ArrayType, index?: number) => boolean;
export type IndexOrPredicateFunction<Type> = number | PredicateFunction<Type>;
export type StatePathKey = IndexOrPredicateFunction<any> | string;
export type StatePath<Obj, Path extends (string | IndexOrPredicateFunction<any>)[] = []> =
object extends Obj
? Path
: Obj extends object
? (Path |
// Check if object is array
(Obj extends readonly any[] ?
// ...when array only allow index or PredicateFunction
StatePath<Obj[number], [...Path, IndexOrPredicateFunction<Obj[number]>]>
// ...when object generate type of all possible keys
: { [Key in string & keyof Obj]: StatePath<Object[Key], [...Path, Key]> }[string & keyof Obj]))
: Path;
This works for e.g. this interface:
interface State1 {
test: {
nestedTest: boolean
}
}
like this:
const t1: StatePath<State1> = ['test', 'nestedTest'];
But it breaks as soon as I have an optional property:
interface State2 {
test: {
nestedTest?: boolean
}
}
Any idea how to solve this? I already tried to use the -?
on the type without success.
Find a sandbox for reproduction here: Typescript playground
The simplest fix for this particular issue would be to change your check from object extends Obj
to object extends Required<Obj>
. If Obj
turns out to be a weak type, meaning it's an object type where all the properties are optional, then TypeScript will see the empty object type {}
and the object
type as assignable to it. So, for example, object extends {a?: string, b?: number}
is true. But then the type will bail out where you don't want it to.
There are various ways to proceed, but by using the Required<T>
utility type you'd compare object
to a version of the type where the optional properties are made required. And while object extends {a?: string, b?: number}
is true, object extends Required<{a?: string, b?: number}>
(a.k.a. object extends {a: string, b: number}
) is false. And so now the type won't bail out unless Obj
really is empty, or object
, or unknown
, etc.