-
-
Notifications
You must be signed in to change notification settings - Fork 148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support dotted path notation #154
Comments
¡Hola Nando! ¿Como mola? We already have something similar here https://millsp.github.io/ts-toolbelt/modules/_object_paths_.html I can start from there and do this for you in the next week, with a better implementation that won't crash on circular references and that is certainly easier to read and understand/maintain :) |
Todo bien! Thank you, great to hear. A key part of the new syntax is that it enforces strict paths for nested strings. I use object paths, but I don't think it quite does the same for nested for notation of only certain paths. Thanks again! |
PS I think your link is broken |
Fixed |
what do you mean? |
Yep, it works correctly, though. The problem here is that TypeScript replaced The implementation I'll work on will have a common limitation with the implementation you linked above: it cannot handle circular references. When this case happens, it won't crash, instead, it will allow any path and not provide auto-completion. Maybe you see a different way of handling this scenario - let me know. |
I limited the output to |
Wow very cool! Is there any shot you could share your temporary solution here to try in the meantime? Thank you again. |
Giving it to you "as is", let me know if we can improve it. I embeds a few safe-guards already. Try to crash it please 🥇 import {A, I, L, M} from 'ts-toolbelt'
type _Paths<O, Paths extends L.List<A.Key> = [], Limit extends I.Iteration = I.IterationOf<'0'>> =
10 extends I.Pos<Limit>
? Paths
: O extends M.BuiltInObject
? Paths
: O extends object
? Paths | {
[K in keyof O]: _Paths<O[K], L.Append<Paths, K>, I.Next<Limit>>
}[keyof O]
: Paths
export type Paths<O extends object> =
_Paths<O>
type Joinable = string | number | boolean | bigint
type Join<
L extends L.List<Joinable>,
D extends Joinable,
J extends Joinable = '',
I extends I.Iteration = I.IterationOf<'0'>> = {
0: Join<L, D, `${J}${D}${L[I.Pos<I>]}`, I.Next<I>>
1: J extends `.${infer J}` ? J : never
}[A.Extends<I.Pos<I>, L.Length<L>>]
type StringPaths<O extends object> =
Paths<O> extends infer P
? P extends unknown
? Join<A.Cast<P, L.List<string | number>>, '.'>
: never
: never declare function get<O extends object, P extends StringPaths<O>>(obj: O, path: P | String): P;
declare const object: O
get(object, '')
type O = {
h: {
b: {
c: {
d: {
e: {
f: O
}
}
}
}
},
b: {
b: {
c: {
d: {
e: {
f: O
}
}
}
}
},
c: {
b: {
c: {
d: {
e: {
f: O
}
}
}
}
},
d: {
b: {
c: {
d: {
e: {
f: O
}
}
}
}
}
} |
Heya, I woke up fresher this morning. Here's a shorter, cleaner version: import {I, M} from 'ts-toolbelt'
type _PathsDot<O, Paths extends string = '', Limit extends I.Iteration = I.IterationOf<'0'>> =
11 extends I.Pos<Limit> ? Paths :
O extends M.BuiltInObject ? Paths :
O extends object ? Paths | {
[K in keyof O]: _PathsDot<O[K], `${Paths}.${K & string}`, I.Next<Limit>>
}[keyof O]
: Paths
export type PathsDot<O extends object> =
_PathsDot<O> extends `.${infer P}` | infer _
? P
: '' declare function get<O extends object, P extends PathsDot<O>>(obj: O, path: P | String): P;
declare const object: O
get(object, '')
type O = {
h: {
b: {
c: {
d: {
e: {
f: O
}
}
}
}
},
b: {
b: {
c: {
d: {
e: {
f: O
}
}
}
}
},
c: {
b: {
c: {
d: {
e: {
f: O
}
}
}
}
},
d: {
b: {
c: {
d: {
e: {
f: O
}
}
}
}
}
} Notice that I added |
Wow, this looks great, I'll test it and report back. Thank you so much! |
Thanks, I'll include this in the next release - around the 15th of November. I will also create |
@millsp This seems to be working great! The only thing is my linter is giving me errors when using Let me know if I should just ignore that. Also, it doesn't seem to support objects nested in arrays at the time. Not the end of the world, but figured I'd mention it in case you'd know of a solution.
Awesome! I assume this means the illustrative Really great work on this overall. |
I saw that earlier, I updated the snippet :) It should work now.
Yes, I just left it like that for now. You can probably use the other implementation for now, though I'm not sure it's 100% type-safe.
Yep. Because if I used
Amazing! Thanks :) Don't hesitate to share this project with others! |
I see one case where the paths cannot be resolved is where you have a list of unknown size - because it could be empty (and I cannot generate a path for all possible indexes). The solution could be to take steps of |
Great news. While programming today, I actually noticed that TypeScript DOES accept to have strings like https://gist.github.com/millsp/1eec03fbe64592c70efa4c80515f741f |
Would this work if I did |
No, we would need to implement something. I quickly bootstrapped something and since it increases complexity, you would be limited to |
If that's ok, I can do that for you (are you building a lib), or was it purely out of curiosity? @nandorojo |
A few different places: I'm using it in my actual app, and I'm also working on adding strict types to Drispy, a design system I maintain. I want to safely use fields such as |
That should still work, I would imagine that |
Right - I basically want to pass a type as a generic, and get all paths that correspond to that type. |
Are you looking for exact types? microsoft/TypeScript#12936 Exact types allow for no more and no less properties than their "model". I have this, unpublished atm. |
@nandorojo, please, would you be so kind to let me know how the "large object" test went, if you have time to test ofc! |
Yeah definitely! |
@millsp any chance this supports my use case mentioned in #154 (comment) (omitting array index)? |
@andreialecu Mongo does not have this yet? It would be awesome if you could get declare function get<O extends object, P extends string>(
object: O, path: AutoPath<MongoIfy<O>, P>
): Path<MongoIfy<O>, S.Split<P, '.'>>
type MongoIfy<A> = {
[K in keyof A]: A[K] extends List
? MongoIfy<A[K][number] | A[K]>
: MongoIfy<A[K]>
}
declare const user: User
type User = {
name: string
friends: User[]
}
// works
const friendName = get(user, 'friends.friends.name')
const friendFriendName = get(user, 'friends.40')
// errors
const friendNames = get(user, 'friends.40.names')
const friendFriendNames = get(user, 'friends.40.friends.12.names') |
I have published the latest bugfixes and tests. I can confirm the there are no performance problems. I tested |
Clever solution.
Nevermind. I was on my phone and didn't read it properly. Seems it already does it. Thanks! |
@millsp I'm using Formik here, creating a checkbox component: import React from 'react'
import { Function } from 'ts-toolbelt'
import { useFieldFast } from '../hooks/use-fast-field'
type CheckboxFieldProps<Schema extends object, Path extends string> = {
name: Function.AutoPath<Schema, Path>
}
export default function CheckboxField<
Schema extends object,
Path extends string
>({ name }: CheckboxFieldProps<Schema, Path>) {
const [{ value }] = useFieldFast<boolean | undefined>(name)
return <></>
}
type Schema = {
user: {
approval: {
isApproved: boolean
adminApprovals: boolean[]
}
}
}
function Test() {
return <CheckboxField<Schema> />
} The problem is, this complains, since I haven't passed a The requirement to pass a second generic argument ahead of time here would kind of defeat the purpose. The problem is, I do indeed need to pass a generic so that the component knows the form type. I tried making Any idea how I can get the |
Yes, this is a terrible TS limitation. prisma/prisma#3372 (comment) Not sure how to make this work in React |
The crux of the issue here appears to be that it cannot infer because one of the generics is required. The only workaround I've found is to do this: type CheckboxFieldProps<Schema, Path extends string> = {
name: Function.AutoPath<Schema, Path>
schema: Schema
}
type User = {
approval: {
isApproved: boolean
adminApprovals: boolean[]
}
}
// then in the component
<CheckboxField schema={null as User} name="approval.isApproved" /> But this is really not ideal, since I'm moving beyond types and casting a fake prop. Is there any other option, you think? |
You are limited by TS here, this impacts all of us on a daily basis. Only workarounds possible. |
In your case, you'd like the previous implementation - but we know this is not possible because of its performance problems and limitations. I have tried to re-implement a bare minimum non-deferred version, but it fails on large objects. Only the new implementation is production ready. Sorry! |
import {
Function,
Object,
String,
} from "ts-toolbelt";
declare function get<Obj extends object, Path extends string>(
object: Obj, path: Function.AutoPath<Obj, Path>
): Object.Path<Obj, String.Split<Path, '.'>>
declare const user: User
type User = {
name: string
friends: User[]
}
// works
const friendName = get(user, 'friends.40.name')
const friendFriendName = get(user, 'friends.40.friends.12.name')
// errors
const friendNames = get(user, 'friends.40.names')
const friendFriendNames = get(user, 'friends.40.friends.12.names') |
I'm actually using the previous PathsDot implementation (mainly for React props), it already works quite nice for me. Has you're tinkering made any improvement on it? If so, I'd love to see it and try to improve on it myself. I tested your new AutoPath btw, it's rock solid, amazing. |
That was my conclusion, sorry. I could not make it reliable enough. While it may work for isolated/small examples, it is not fail-proof at all. There is no way to improve it - it WILL fail when you hit that object that is a bit too big. TypeScript needs to get more agressive optimizations - thus the deferred version is the only answer here. But you can use it at your own risk locally :) We are bound to this limitation for now, to get it to work with react prisma/prisma#3372 (comment) |
I'm trying to achieve something similar with dot nested paths like so: <T extends Record<string, any>, K extends string> (obj: T, keys: K[]): O.Pick<T, K>
// support dot paths for `K` at the end
// tried:
<T extends Record<string, any>, K extends string> (obj: T, keys: K[]): O.Pick<T, F.AutoPath<T, K>> But I'm not sure I correctly understand AutoPath Could you point me in the correct direction? : ) This is what I want to achieve: const doc = { a: { b: { yes: 0, no: 0 } } }
const res = pick(doc, ['a.b.yes']) Currently the type of res // {} but I want it to be: res // { a: { b: { yes: number } } } |
I think that you're looking for declare function pick<Obj extends object, Path extends string>(
object: Obj, path: Function.AutoPath<Obj, Path>
): Object.P.Pick<Obj, String.Split<Path, '.'>> |
@millsp this new TS feature might be relevant for this library |
@millsp I am unfortunately not that familiar with ts-toolbelt so I am not sure if this is a bug. But the example for AutoPath shown in CodeSandbox (and also in my current project) does not show any errors for the two lines under "//errors": type User = {
name: string
friends: User[]
}
// works
const friendName = get(user, 'friends.40.name')
const friendFriendName = get(user, 'friends.40.friends.12.name')
// errors
const friendNames = get(user, 'friends.40.names')
const friendFriendNames = get(user, 'friends.40.friends.12.names') Link CodeSandbox: https://codesandbox.io/s/x4jly?file=/src/index.ts:3130-3379 Everything that I write after the index of the array is not typed anymore and does not produce any type errors. Is here something missing? I would be very happy if you could take a look at it :) |
FWIW I’ve also been using |
@millsp Facing same issue as @nandorojo, I just copied the same from docs and still no errors were thrown on invalid path. |
Looks like there is an open issue, version 9.3.0 works. |
react-hook-form |
I've extracted the Types from /**
* Checks whether T1 can be exactly (mutually) assigned to T2
* @typeParam T1 - type to check
* @typeParam T2 - type to check against
* ```
* IsEqual<string, string> = true
* IsEqual<'foo', 'foo'> = true
* IsEqual<string, number> = false
* IsEqual<string, number> = false
* IsEqual<string, 'foo'> = false
* IsEqual<'foo', string> = false
* IsEqual<'foo' | 'bar', 'foo'> = boolean // 'foo' is assignable, but 'bar' is not (true | false) -> boolean
* ```
*/
export type IsEqual<T1, T2> = T1 extends T2
? (<G>() => G extends T1 ? 1 : 2) extends <G>() => G extends T2 ? 1 : 2
? true
: false
: false;
export type Primitive =
| null
| undefined
| string
| number
| boolean
| symbol
| bigint;
export type BrowserNativeObject = Date | FileList | File;
/**
* Type which given a tuple type returns its own keys, i.e. only its indices.
* @typeParam T - tuple type
* @example
* ```
* TupleKeys<[number, string]> = '0' | '1'
* ```
*/
export type TupleKeys<T extends ReadonlyArray<any>> = Exclude<
keyof T,
keyof any[]
>;
/**
* Type to query whether an array type T is a tuple type.
* @typeParam T - type which may be an array or tuple
* @example
* ```
* IsTuple<[number]> = true
* IsTuple<number[]> = false
* ```
*/
export type IsTuple<T extends ReadonlyArray<any>> = number extends T['length']
? false
: true;
/**
* Type which can be used to index an array or tuple type.
*/
export type ArrayKey = number;
/**
* Helper function to break apart T1 and check if any are equal to T2
*
* See {@link IsEqual}
*/
type AnyIsEqual<T1, T2> = T1 extends T2
? IsEqual<T1, T2> extends true
? true
: never
: never;
/**
* Helper type for recursively constructing paths through a type.
* This actually constructs the strings and recurses into nested
* object types.
*
* See {@link Path}
*/
type PathImpl<K extends string | number, V, TraversedTypes> = V extends
| Primitive
| BrowserNativeObject
? `${K}`
: // Check so that we don't recurse into the same type
// by ensuring that the types are mutually assignable
// mutually required to avoid false positives of subtypes
true extends AnyIsEqual<TraversedTypes, V>
? `${K}`
: `${K}` | `${K}.${PathInternal<V, TraversedTypes | V>}`;
/**
* Helper type for recursively constructing paths through a type.
* This obscures the internal type param TraversedTypes from exported contract.
*
* See {@link Path}
*/
type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes>;
}[TupleKeys<T>]
: PathImpl<ArrayKey, V, TraversedTypes>
: {
[K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes>;
}[keyof T];
/**
* Type which eagerly collects all paths through a type
* @typeParam T - type which should be introspected
* @example
* ```
* Path<{foo: {bar: string}}> = 'foo' | 'foo.bar'
* ```
*/
// We want to explode the union type and process each individually
// so assignable types don't leak onto the stack from the base.
export type Path<T> = T extends any ? PathInternal<T> : never;
type User = {
name: string
friends: {
test: string;
}[];
}
type Keys = Path<User>;
const test: Keys = "friends.3.test";
console.log(test); |
🍩 Feature Request
Is your feature request related to a problem?
TypeScript 4.1 has template literals, and they're super useful. However, the only solutions out there are quite hacky to use, and result in some bugs.
I want to be able to get a nested object path, as shown in this repo.
Describe the solution you'd like
I want to know how do I get the nested object path using dot notation of an object. I'd then like to get the value that matches that object with the path.
I'd like to achieve this:
This is the idea:
However, this code block above seems to be buggy. I occasionally get infinite loop errors from typescript if I use it (but not always), so someone who has a better understanding of TS than I do should probably make it.
Describe alternatives you've considered
I tried the code sample above. However, it is not very robust. Someone put it on twitter, but I think it would make more sense for it to live in TS toolbelt (which is always so helpful.)
Teachability, Documentation, Adoption, Migration Strategy
This is a great article describing the template literals. Surprisingly, if you google "TypeScript template literals," it's hard to find content, even though it's such a cool addition. Do you think you might be able to help @millsp? Thank you so much!
The text was updated successfully, but these errors were encountered: