Generic function to get a nested object value - node.js

The problem itself is more complex than this but I'll try to explain in a simple way.
I have an object (it could be a different object, lets say a car o user or whatever).
const car = {
model: 'Model A',
specs: {
motor: {
turbo: true
}
}
};
Then I have a function to get a value from a property name.
interface Data<T, K extends keyof T> {
keynames: string[];
values: Array<T[K] | null>;
}
const getValues = <T, K extends keyof T>(keynames: string[], data: T): Data<T, K> => {
const values: Array<T[K] | null> = [];
keynames.forEach(keyname => {
const value: T[K] | undefined = getObjectValue(data, keyname);
if (typeof value === 'undefined') {
values.push(null);
} else {
values.push(value);
}
});
return {
keynames,
values
}
};
// This works with key.name syntax, specs.motor.turbo returns true for example
const getObjectValue = <T, K extends keyof T>(
object: T,
keyName: string
): T[K] | undefined => {
const keys = keyName.split('.');
if (keys.length === 1) {
if (typeof object === 'object') {
if (keys[0] in (object as T)) {
return (object as T)[keys[0] as K];
}
return undefined;
}
return undefined;
} else {
const [parentKey, ...restElements] = keys;
if (!object) return undefined;
return (getObjectValue(
(object as T)[parentKey as K],
restElements.join('.')
) as unknown) as T[K];
}
};
The problem becomes when I write some tests for example:
// ...
const data = getValues(['model', 'specs.motor.turbo'], car);
assert.deepEqual(data, {
keynames: ['model', 'specs.motor.turbo'],
values: ['Model A', true],
});
// ...
data is what I'm expecting, my function returns the same object as expected but I'm getting an error:
Type 'boolean' is not assignable to type 'string | { motor: { turbo: boolean; }; }'.ts(2322)
which is obvious since I'm only inferring T[K] in my function and not the type of the nested properties.
How to achieve that, so my function works with return types string | { motor: { turbo: boolean; }; } | { turbo: boolean; } | boolean. Or maybe there is a simpler way to return a nested property value.

I'm going to preserve your implementation as much as possible, even though there might be some improvements there that someone would suggest. I'm primarily taking this question as "how can I tell the TypeScript compiler what getValues() is doing?".
First, we need to represent what comes out when you deeply index into a type T with a dotted keypath K. This is only going to be possible in TypeScript 4.1 and later, since the following implementation relies both on template literal types as implemented in microsoft/TypeScript#40336, and recursive conditional types as implemented in microsoft/TypeScript#40002:
type DeepIndex<T, K extends string> = T extends object ? (
string extends K ? never :
K extends keyof T ? T[K] :
K extends `${infer F}.${infer R}` ? (F extends keyof T ?
DeepIndex<T[F], R> : never
) : never
) : never
The general plan here is to check if K is a key of T; if so, we do the lookup with T[K]. Otherwise we split K at the first dot into F and R, and recurse downward, indexing into T[F] with the key R. If at any point something goes wrong (F is not a valid key of T, for example), this returns never instead of property type. We will use this; if something becomes never then we assume that there was a bad index:
type ValidatePath<T, K> =
K extends string ? DeepIndex<T, K> extends never ? never : K : never;
The type ValidatePath<T, K> will extract from the (possible union of keys) K just those members which are valid paths.
Before we get into it, let's also represent what you do when you replace undefined with null:
type UndefinedToNull<T> = T extends undefined ? null : T;
For your code, I'm going to go ahead and use tuple types to represent the keynames and values arrays in Data. This should fall back to unordered arrays if necessary, but it seems weird to throw away information you know; for example, on your car example, you know that the first element of the values array is a string and the second is a boolean. A type like [string, boolean] has more information than Array<string | boolean | null>:
interface Data<T, K extends string[]> {
keynames: K;
values: { [I in keyof K]: UndefinedToNull<DeepIndex<T, Extract<K[I], string>>> };
}
The values property uses a mapped array/tuple to convert the tuple of keynames in K to a tuple of deep indexed values.
Now we need to give strong types to your function signatures. The compiler won't be able to verify a lot of the type safety inside of the implementations, so I'll be doing a lot of type assertions with as to force the compiler to accept what we're doing:
const getValues = <T, K extends string[]>(keynames: (K & { [I in keyof K]: ValidatePath<T, K[I]> }) | [], data: T): Data<T, K> => {
const values = [] as { [I in keyof K]: UndefinedToNull<DeepIndex<T, Extract<K[I], string>>> };
const _keynames = keynames as K;
_keynames.forEach(<I extends number>(keyname: K[I]) => {
const value: DeepIndex<T, Extract<K[I], string>> | undefined = getObjectValue(data, keyname as any);
if (typeof value === 'undefined') {
values.push(null as UndefinedToNull<DeepIndex<T, K[I]>>);
} else {
values.push(value as UndefinedToNull<DeepIndex<T, K[I]>>);
}
});
return {
keynames: _keynames,
values
}
};
const getObjectValue = <T, K extends string>(
object: T,
keyName: K & ValidatePath<T, K>
): DeepIndex<T, K> | undefined => {
const keys = keyName.split('.');
if (keys.length === 1) {
if (typeof object === 'object') {
if (keys[0] in (object as T)) {
return (object as T)[keys[0] as keyof T] as DeepIndex<T, K>;
}
return undefined;
}
return undefined;
} else {
const [parentKey, ...restElements] = keys;
if (!object) return undefined;
return (getObjectValue(
(object as T)[parentKey as keyof T],
restElements.join('.') as any as never
) as unknown) as DeepIndex<T, K>;
}
};
I wouldn't worry too much about the assertions inside the implementation. The important piece here is the call signature to getValues():
<T, K extends string[]>(keynames: (K & { [I in keyof K]: ValidatePath<T, K[I]> }) | [], data: T) => Data<T, K>
We are interpreting the keynames as something assignable to an array of string values. The | [] at the end is just a hint that the compiler should prefer viewing, say, ['model', 'specs.motor.turbo'] as an ordered pair instead of an unordered array. The intersection with {[I in keyof K]: ValidatePath<T, K[I]>}, a mapped tuple, should be a no-op is all the elements of K are valid paths into T. If any element of K is an invalid path, though, that element of the mapped tuple will be never and the validation will fail.
Let's test it:
const car = {
model: 'Model A',
specs: {
motor: {
turbo: true
}
}
}
const data = getValues(['model', 'specs.motor.turbo'], car);
data.keynames; // ["model", "specs.motor.turbo"]
data.values; // [string, boolean]
console.log(data);
This looks like exactly what you want with your example. What happens if we misspell a key?
getValues(['model', 'specs.motar.turbo'], car); // error!
// ---------------> ~~~~~~~~~~~~~~~~~~~
// Type 'string' is not assignable to type 'undefined' 🤷‍♂️
We get an error on the offending key. The error message isn't that useful, unfortunately. When I tried to use the answer to this question to give the compiler a full list of exactly which dotted paths to accept, it caused a massive slowdown and even some "type instantiation is excessively deep or infinite" errors. So while it would be nice to see "specs.motar.turbo" is not assignable to "model" | "specs" | "specs.motor" | "specs.motor.turbo"`, sadly I can't get that to happen in a reasonable way.
What if there could be an undefined value at the property in question?
const d2 = getValues(['a.b'], { a: Math.random() < 0.5 ? {} : { b: "hello" } });
d2.keynames; // ["a.b"]
d2.values; // [string | null]
console.log(d2);
It comes out as | null instead of | undefined, which is good.
So that works as well as I could get it to. There are undoubtedly limitations and edge cases. Deep indexing with dotted keys is kind of near the edge of what works in TypeScript (before 4.1 is was significantly past the edge, so that's something, right?). For example, I'd expect weird/bad things to happen with optional or union-typed properties at non-leaf nodes of the object tree. Or objects with string index signatures, for that matter. These might be addressable, but it would take some effort and a lot of testing. The point is: tread carefully.
Playground link to code

Related

Retrieve inferred string literal from object keys

I'm trying to create a type that defines the value based on the key. If the key extends $${string} (e.g. $foo) the value should be the key without the prefix e.g. foo. If the key doens't extend $${string} (e.g. boo) the values should be null.
Example
const example = {
$foo: 'foo',
boo: null,
}
Here is an isolated example I created to get it done - but it doesn't work as intended when I apply it to the code below. 😕
type Value<T> = T extends `$${infer I}` ? I : null
type ExampleA = Value<'$foo'> // type is 'foo'
type ExampleB = Value<'boo'> // type is null
My current code
type Values = {
[K in string]: K extends `$${infer p}` ? p : null;
}
const values = {
$foo: 'foo', // Type 'string' is not assignable to type 'null'.
foo: null,
$boo: 'boo', // Type 'string' is not assignable to type 'null'.
boo: null,
} satisfies Values;
type Expected = {
readonly $foo: 'foo',
readonly foo: null,
readonly $boo: 'boo',
readonly boo: null,
}
The satisfies Values is used to infer the type later on. Similar approach is acceptable🙂
Thanks for your help and time - cheers
The problem with your Values type is that mapped types over string do not behave the way you expect them to. While conceptually you can think of string as the infinite union of all possible string literal types, a mapped type over string does not even try to iterate over every possible string literal type; it just maps one thing: string:
type Values = {
[K in string]: K extends `\$${infer S}` ? S : null;
}
/* type Values = {
[x: string]: null;
} */
And since string does not extend `\$${infer S}`, then the property type for the string key is null.
This is working as intended, as discussed in microsoft/TypeScript#22509. Mapped types over string are not what you want.
And unfortunately there is no way to write a specific type in TypeScript which behaves the way you want. The closest you could get is something like
type Values = {
[k: `\$${string}`]: string;
[k: string]: string | null;
}
using a template string pattern index signature, but the parts where the property value string needs to match the part after the "$" character (not just string) and the part where other keys need to have a null (not just string | null) cannot be represented:
const values = {
$foo: 'foo',
foo: null,
$boo: 'boo',
boo: null,
$oops: null, // error, not string
oops: 'hmm', // should be error, but isn't!
$whoops: 'oops', // should be error, but isn't!
} satisfies Values;
So we have to give up on the approach using the satisfies operator, because there is no appropriate Values type to use it with.
What you really care about is having the type of values inferred by the compiler but still checked against your desired constraint. We can get behavior like this by replacing satisfies Values with a generic helper function we can call satisfiesValues(). At runtime this function just returns its input, but the compiler can use it to validate the object literal passed in. So instead of const values = {...} satisfies Values; you would write const values = satisfiesValues({...});.
Here's one possible implementation:
const satisfiesValues = <K extends PropertyKey>(
val: { [P in K]: P extends `\$${infer S}` ? S : null }
) => val;
The function is generic in K, the keys of the val value passed in. This will most likely be some union of known keys (none of which will be just string), and then the mapped type behaves as desired:
const values = satisfiesValues({
$foo: 'foo',
foo: null,
$boo: 'boo',
boo: null,
$oops: null, // error, not "oops"
oops: 'hmm', // error, not null
$whoops: 'oops', // error, not "whoops"
});
/* const values: {
foo: null;
$foo: "foo";
boo: null;
$boo: "boo";
oops: null;
$whoops: "whoops";
$oops: "oops";
} */
Looks good. The type of values is what you want it to be, and the compiler allows the valid properties and complains about the invalid ones.
Playground link to code

Get keys of an interface, in generics

I have a function that uses generics. The generics in question is just <T extends anInterfaceName>. The problem is, in the function I need to get the keys of the interface used in T. After doing some research, i found this module.
Problem is, it doesn't work with generics. I can't seem to find anything that can help, is there anything that could do this? Is there a work around to this problem?
Here you have several wayt to get keys:
interface Keys {
name: string;
age: number;
}
const fun1 = <T,>(obj: T, key: keyof T): T[keyof T] => null as any
const fun2 = <T, K extends keyof T>(obj: T, key: K): T[K] => null as any
const fun3 = (key: keyof Keys) => null
const fun4 = <T extends Keys, K extends keyof T>(obj: T, key: K) => null
If above code does not work for you, please provide an example what are you expect.
UPDATE
/**
* UPDATE
*/
const fun5 = <T,>(): Array<keyof T> => null as any
const fun6 = <T,>(): (keyof T)[] => null as any
const result = fun5<Keys>() // ("name" | "age")[]
UPDATE 2
If you want more strict type of array, you can use next util:
//Credits goes to https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
// Credits goes to ShanonJackson https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<U extends any ? (f: U) => void : never>;
// Credits goes to ShanonJackson https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type PopUnion<U> = UnionToOvlds<U> extends ((a: infer A) => void) ? A : never;
// Credit goes to Titian Cernicova-Dragomir https://stackoverflow.com/questions/53953814/typescript-check-if-a-type-is-a-union#comment-94748994
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true
// Finally me)
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true ? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]> : [T, ...A]
interface Person {
name: string;
age: number;
surname: string;
children: number;
}
type Result = UnionToArray<keyof Person>
const func = <T,>(): UnionToArray<keyof T> => null as any
const result = func<Person>() // ["name", "age", "surname", "children"]
I really don't know other wayt to get keyof's.
Please keep in mind, this solution is not CPU friendly and there is no order guarantee.
UPDATE 3
Here is much safer option to obtain keys. Next solution takes into account that order could not be preserved:
type TupleUnion<U extends string, R extends any[] = []> = {
[S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U];
interface Person {
firstName: string;
lastName: string;
dob: Date;
hasCats: false;
}
type keys = TupleUnion<keyof Person>;//
Shamelessly stolen from Wroclaw twitter TS group
I think you are using Typescript in the wrong way.
If you are really interested in going that path you probably can use Typeguards.
https://www.typescriptlang.org/docs/handbook/advanced-types.html
but the problem here is that anInterfaceName needs to have the keys of what you are interested to grab, T extends anInterfaceName means I can be anything that has the anInterfaceName keys, so there's an infinite possibilities of T classes..

How to limit function input to dynamically assigned keys of an object in typescript?

lately I am trying to create a placeholder object that I will be able to use with typescript.
Idea is to have an empty object and two functions:
one to add new key to a placeholder object with another object as a value ( 'add' function )
and one to get this value by passing a key that already exists in a placeholder ( 'get' function )
I would like typescript to forbid to type keys that already exist in placeholder in 'add' function.
Also I would like to get suggestions while typing key in 'get' function.
Last thing that I would like to achieve is to have type of an object that is returned from 'get' function instead of 'any' or 'object'
Here is the sample code with some basic typing:
let placeholder = {}
function add(key: string, test: object) {
placeholder[ key ] = test
}
function get(key: string ) {
return placeholder[key]
}
add('test1', { val: 1 }) // here 'test1' is ok
add('test1', { val: 2 }) // here 'test1' should rise an error
let t1 = get('') // here 'test1' and should be suggested
t1.val // here t1 should have type { val: number }
So far I have tried using generic types with things like:
function add( key: Omit< string, keyof typeof placeholder >, test: object ) { ... } // it is casting key to properties of string
function get< Key extends keyof typeof placeholder > ( key: Key ) { ... } // it only works with static keys
That is not possible. This would require the type of the object to change, but types are static.
The only thing you could do would be to return an object from your add function with a modified type and then continue calls on that object.
Example of that approach (still has one typing issue i set to ignore):
class Placeholder<T>
{
constructor(private entries: T)
{
}
add<K extends string, V>(key: Exclude<K, keyof T>, value: V): Placeholder<T & { [X in K]: V }> {
const newEntries = { ...this.entries, [key]: value };
// #ts-ignore
return new Placeholder(newEntries);
}
get(key: keyof T) {
return this.entries[key];
}
}
const ph = new Placeholder({})
.add('test1', { val: 1 }) // here 'test1' is ok
.add('test1', { val: 2 }) // here 'test1' should rise an error
let t1 = ph.get('test1') // here 'test1' and should be suggested
t1.val // here t1 should have type { val: number }
[Playground Link]

Extract "defined" type from property in TypeScript at runtime

What I want to do
I'm currently looping over an object's keys and transferring the values to another object.
interface From {
[key: string]: string;
}
let from: From = {
prop1: "foo",
prop2: "23",
};
interface To {
[key: string]: string | number | boolean | undefined;
prop1: string;
prop2: number;
prop3: boolean;
}
let to: To = {} as To;
const initEnv = async () => {
const keys: string[] = Object.keys(from);
for (let i = 0; i < keys.length; i++) {
let key: string = keys[i];
let keyType = typeof (key as keyof To); // This only returns "string". Which kind of makes sense to me
let keyType = typeof keyof key; // SyntaxError: ',' expected
let keyType: typeof to[key]; // Error: 'key' refers to a value, but is being used as a type
to[key] = from[key];
}
};
I would want to be able to, say switch the value, so I don't want to just extract the type of the key. I want to assign it to a variable for use in the code, thus at runtime, as a string for instance.
So I think things like this wouldn't work.
let keyType2: typeof env[key]; // Error: 'key' refers to a value, but is being used as a type
Maybe, but the question is then; what do I assign to this variable?
The reason for all this, is that I want to convert the from variables to the correct type before, assigning them to the to object.
So yeah, basically, my question is how I would extract the type (as a string, at runtime, dynamically) from the key. Or is it even possible in the first place? And if it isn't why not? I like understanding things.
Thanks for putting up with my bad english, as well.
There are no interfaces at runtime; TypeScript's type system is erased from the emitted JavaScript. Your from and to values will be evaluated like this at runtime:
let from = {
prop1: "foo",
prop2: "23",
};
let to = {};
There's no From or To, and no way to use To to figure out how to coerce from's properties into the right types. The type system has no runtime effects.
The usefulness of TypeScript's type system comes from describing what will happen at runtime and not from affecting things at runtime. Imagine how you would have to write your code in pure JavaScript, and then give types to that code. Here's one way I might do it. Instead of a To interface, let's make a To object whose properties are functions that coerce inputs to other types:
const To = {
prop1: String,
prop2: Number,
prop3: Boolean
}
This is enough information to proceed at runtime.
Now, if you were going to build to manually, the compiler would be able to understand that the resulting value has a prop1 property of type string and a prop2 property of type number:
const toManual = { prop1: To.prop1(from.prop1), prop2: To.prop2(from.prop2) };
/* const toManual: { prop1: string; prop2: number; } */
But you don't want to do it manually; you'd like to write a loop that walks through the keys of from and uses To to produce properties of to. This is harder for the compiler to understand, but with judicious use of type assertions and type annotations you can write an objMap function that works programmatically:
function objMap<T, F extends { [K in keyof T]: (arg: T[K]) => any }>(
obj: T, fMap: F) {
const ret = {} as { [K in keyof T]: ReturnType<F[K]> };
const fM: { [K in keyof T]: (arg: T[K]) => ReturnType<F[K]> } = fMap;
(Object.keys(obj) as Array<keyof T>).forEach(<K extends keyof T>(k: K) => {
ret[k] = fM[k](obj[k]);
})
return ret;
}
The objMap function takes an object obj and a mapping object fMap which has at least all the same keys as obj and whose properties are functions that map obj's properties. The return type of objMap is an object whose properties are all the returned values of the fMap function for each property in obj. The actual work is being done by ret[k] = fM[k](obj[k]);. It's the programmatic equivalent of ret.prop1 = To.prop1(from.prop1); and ret.prop2 = To.prop2(from.prop2).
Let's see if it works:
const to = objMap(from, To);
/* const to: { prop1: string; prop2: number; } */
console.log(JSON.stringify(to));
// {"prop1":"foo","prop2":23}
That looks correct; the type of to is inferred by the compiler to be {prop1: string, prop2: number}, and the actual value of to is computed at runtime to be {prop1: "foo", prop2: 23}.
Okay, hope that helps; good luck!
Playground link to code

Unexpected "Spread types may only be created from object types" error when using generics

I've got this typescript class that requires a generic type to be provided on construction:
type Partial<T> = {
[P in keyof T]?: T[P];
};
class Foo<Bar> {
bis: Partial<Bar> = {}; // (1)
constructor() {
console.log(typeof this.bis); // object
this.bis = {...this.bis}; // (2) Spread types may only be created from object types
}
}
How ever, as you can see above, i don't get an error at (1), but i do at (2).
Why is this? And how do i fix it?
Edit1:
I've opened an issue over at the Typescript github.
A workaround for this is typecasting the object explicitely with <object>,<any> or <Bar> in your case.
I don't know if your requirements allow this or not but have a look -
type Partial<T> = {
[P in keyof T]?: T[P];
};
class Foo<Bar> {
bis: Partial<Bar> = {}; // (1)
constructor() {
console.log(typeof this.bis); // object
this.bis = {...<Bar>this.bis};
}
}

Resources