Skip to content

Instantly share code, notes, and snippets.

@ajitid
Last active June 25, 2022 22:56
Show Gist options
  • Save ajitid/5685e908f5ea05fc155e0ffe5a3652fa to your computer and use it in GitHub Desktop.
Save ajitid/5685e908f5ea05fc155e0ffe5a3652fa to your computer and use it in GitHub Desktop.

Note: You'll need at least TypeScript 4.1

Usage

interface Shape {
  a: {
    b: string 
    c: number
  },
  d: number
}

const key = 'a.c'
type Value = Dotted<typeof key, Shape>

const value1: Value = 7            // passes
const value2: Value = 'a'          // fails
const value3: Value = {'no': 'pe'} // fails
type ValueType<
S extends string,
R extends Record<string, any>
> = S extends `${infer T}.${infer U}` ? ValueType<U, R[T]> : R[S];
export type Dotted<
S extends string,
R extends Record<string, any>
> = ValueType<S, R> extends infer A
? unknown extends A
? never
: A
: never
/* Something like this can work too but then we'll have to evaluate ValueType two times
export type Dotted<
S extends string,
R extends Record<string, any>
> = unknown extends ValueType<S, R> ? never : ValueType<S, R>;
*/
/*
Lowdb allows retrieving values using indexes like `a[0].b`. For that we can use the following `ValueType` instead.
Note that using key as:
1. `""` (empty)
2. `"..."` (has multiple dots in b/w)
3. `"a.b."`(starts/ends with dot(s))
4. `"a.b[]"` (empty index)
5. `"a.b[cd]"` (non-numeric index)
6. `"a.b[2]"` (expecting a tuple value)
will silently fail.
*/
type ValueType<
S extends string,
R extends Record<string, any> | any[]
> =
S extends ""
? R
: S extends `.${infer Rest}`
? ValueType<Rest, R>
: S extends `[${infer Idx}]${infer Rest}`
? R extends any[]
? ValueType<Rest, R[0]>
: never
: R extends Record<string, any>
? S extends `${infer Key}.${infer Rest}`
? Key extends `${infer ActualKey}[${infer ActualRest}]`
? ActualKey extends ""
? ValueType<`[${ActualRest}].${Rest}`, R>
: ValueType<`[${ActualRest}].${Rest}`, R[ActualKey]>
: ValueType<Rest, R[Key]>
: S extends `${infer ActualKey}[${infer ActualRest}]`
? ValueType<`[${ActualRest}]`, R[ActualKey]>
: R[S]
: never
/*
You can make over `ValueType` more strict by passing into following validation checks. It resolves 2, 3 and 4.
*/
type FailOnMoreThanOneDot<
S extends string | never
> = S extends `${infer A}..${infer B}` ? never : S;
type FailOnDotAtSides<S extends string | never> = S extends
| `.${infer A}`
| `${infer A}.`
? never
: S;
type FailOnEmptyIndex<
S extends string | never
> = S extends `${infer A}[]${infer B}` ? never : S;
// Use this over `ValueType` if you want validation checks too
// Make sure to update occurences of `ValueType` to `WrappedValueType` inside `Dotted`
type WrappedValueType<
S extends string,
R extends Record<string, any>
> = ValueType<FailOnMoreThanOneDot<FailOnDotAtSides<FailOnEmptyIndex<S>>>, R>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment