flitbit / json-ptr

A complete implementation of JSON Pointer (RFC 6901) for nodejs and modern browsers.
MIT License
90 stars 28 forks source link

Type safe JsonPointer.get #52

Open brandonryan opened 2 years ago

brandonryan commented 2 years ago

I made this type because I wanted a type safe way to pull properties from an object via a path, and I thought it might be useful to this library. I chose to make any properties not found in the object never, but you could use unknown to keep current functionality if you wanted. Just thought I'd share.

type TakeProp<T extends string> = T extends `${infer Prop}/${string}` ? Prop : never

type TakeRest<T extends string> = T extends `${string}/${infer Rest}` ? Rest : T

type PathExtract<Value, Path extends string> = 
    TakeProp<Path> extends never //if path type is not a path, get the prop from Value
        ? Path extends keyof Value
            ? Value[Path]
            : never
        : TakeProp<Path> extends keyof Value //check if prop is in value
            ? PathExtract<Value[TakeProp<Path>], TakeRest<Path>> //recurse
            : never //failure

const num: number = PathExtract<{a: {b: number}}, "a/b">

This currently does not cover ~0 and ~1 but i think with a little more work that could be figured out.

brandonryan commented 2 years ago

Updated version that allows for number indexed types (like array)

type TakeProp<T extends string> = T extends `${infer Prop}/${string}` ? Prop : never
type TakeRest<T extends string> = T extends `${string}/${infer Rest}` ? Rest : T
export type PathExtract<Value, Path extends string> = 
    TakeProp<Path> extends never ? //if path type is not a path, get the prop from Value
        Path extends `${number}` ? //if we are dealing with a number index
            number extends keyof Value ? Value[number] : unknown
        : Path extends keyof Value ? Value[Path] : unknown //otherwise just regular prop get
    : TakeProp<Path> extends `${number}` ? //try to see if prop is a number
        number extends keyof Value ?
            PathExtract<Value[number], TakeRest<Path>>
        : unknown
    : TakeProp<Path> extends keyof Value ? //check if prop is in value
        PathExtract<Value[TakeProp<Path>], TakeRest<Path>> //recurse
    : unknown //failure
brandonryan commented 2 years ago

Finalized version. This one can handle decoding as well. Also cleaned up the types

//This is a big utility type to let us path walk a type with a string
export type SplitPath<T extends string> = T extends `${infer Prop}/${infer Rest}` ? [Prop, Rest] : [unknown, T]

//Deal with json pointer encoding
export type Decode<T extends string> = 
    T extends `${infer A}~0${infer B}` ? `${Decode<A>}~${Decode<B>}` :
    T extends `${infer A}~1${infer B}` ? `${Decode<A>}/${Decode<B>}` :
    T

export type ExtractProp<Value, Prop extends string> = 
    //try to see if prop is a number
    Prop extends `${number}` ? 
        number extends keyof Value ? Value[number] :
        unknown :
    //check if prop is in value
    Decode<Prop> extends keyof Value ? Value[Decode<Prop>] : unknown

export type PathExtract<Value, Path extends string> =
    Extract<Value, SplitPath<Path>[0], SplitPath<Path>[1]>

export type Extract<Value, Prop, Rest extends string> = 
    Prop extends string ? PathExtract<ExtractProp<Value, Prop>, Rest> :
    ExtractProp<Value, Rest>
amir-arad commented 1 year ago

amazing job! so we have template literals now in TS? been waiting for soo long!