Extracting an interface from an array of keys of an object to perform Pick and Omit - typescript-typings

I'm having trouble coming up with the necessary typings for the following two functions:
const pick = (obj, ...names) =>
names.reduce((ret, next) => {
if (typeof obj[next] != 'undefined') ret[next] = obj[next];
return ret;
}, {});
const strip = (obj, ...names) =>
pick(obj, ...Object.keys(obj).filter(key => !names.includes(key)));
I've tried something like the following:
type Picker = <T extends object, U extends (keyof T)[]>(obj: T, ...names: U) =>
Pick<U, T>;
type Stripper = <T extends object, U extends (keyof T)[]>(obj: T, ...names: U) =>
Omit<U, T>;
But those types require passing a type T that extends object, not array of strings. How can I construct a type from Array<keyof T> to pass into Pick and Omit?

After rephrasing the question a couple of times, it occured to me that you could create a union type from an array by using the index accessor:
type Picker = <U extends object, V extends (keyof U)[]>(obj: U, ...keys: V) =>
Pick<U, V[number]>;
type Stripper = <U extends object, V extends (keyof U)[]>(obj: U, ...keys: V) =>
Omit<U, V[number]>;

Related

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..

Generic function to get a nested object value

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

TypeScript: Catch variable signature of a function passed as argument in higher order function

I would like for a higher order function to be able to catch the signature parameters of the passed function which can have different signature.
I don't know if it's feasible but this was my approach to it :
type FuncA = (a: string, b: number) => void
type FuncB = (a: string) => void
type Func = FuncA | FuncB
const a: FuncA = (a: string, b: number) => {
console.log('FuncA')
}
const b: FuncB = (a: string) => {
console.log('FuncB')
}
// My higher order function
const c = (func: Func) => {
// do something here...
return (...args: Parameters<typeof func>) => {
func(...args) // Expected 2 arguments, but got 0 or more. ts(2556). An argument for 'a' was not provided.
}
}
My higher order function c couldn't pass the parameters of func
It seems like TypeScript cannot discriminate the different possible signature of type Func.
Does anyone know a pattern to write this kind of code?
Thank you !
This is a tough one because for a function to extend another function doesn't mean quite what you think.
We want the function created by c to require that arguments correspond to the function that it was given. So we use a generic to describe the function.
const c = <F extends Func>(func: F) => {
return (...args: Parameters<F>) => {
func(...args); // still has error
}
}
At this point we still have that error, but when we call c, we get a function which has the right arguments based on whether we gave it a or b.
const cA = c(a); // type: (a: string, b: number) => void
cA("", 0);
const cB = c(b); // type: (a: string) => void
cB("");
As for the error, it has to do with what it means for a function to extend another function. Try changing F extends Func to F extends FuncA and F extends FuncB to see what happens. With F extends FuncB we get an error on c(a), but with F extends FuncA we don't get an error on c(b). Huh?
If you think about it in terms of a callback it makes sense. It's ok to pass a function that requires less arguments than expected, but not ok to pass one that requires more. But we are the ones implementing the callback so this creates a problem for us. If we extend type Func with a function that has no arguments, the empty array from Parameters<F> isn't sufficient to call either type.
We have to make our generic depend on the arguments instead.
type AP = Parameters<FuncA> // type: [a: string, b: number]
type BP = Parameters<FuncB> // type: [a: string]
type Args = AP | BP;
const c = <A extends Args>(func: (...args: A) => void) => {
return (...args: A) => {
func(...args) // no error
}
}
Typescript Playground Link
If you're ok with the decorated function being any function, you could do:
const c = <T extends (...a: any) => any>(func: T) => {
// do something here...
return (...args: Parameters<typeof func>): ReturnType<T> => {
return func(...args);
}
}
Calling it would look like
c<typeof a>(a)('a', 2)

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]

Cannot resolves symbol X, when defining multiple implicit vals

I am testing out some code shown below that basically defines multiple implicit vals taking a string as input and converting it to corresponding types.
The problem I have is that the conversions like toLong, toDouble and toInt become unresolved for some reason.
class Parse[T](val f: String => T) extends (String => T) {
def apply(s: String): T = f(s)
}
object Parse {
def apply[T](f: String => T) = new Parse[T](f)
implicit val parseLong: Parse[Long] = Parse[Long](s => s.toLong)
implicit val parseDouble: Parse[Double] = Parse[Double](s => s.toDouble)
implicit val parseInt: Parse[Int] = Parse[Int](s => s.toInt)
}
What is wrong with this code?
The thing is that, since Parse extends String => T, implicits parseLong, parseDouble, parseInt define not only instances of Parse but also implicit conversions String => Long, String => Double, String => Int. And since .toLong, .toDouble, .toInt are extension methods, this creates ambiguities.
You can either remove extends (String => T) or resolve extension methods manually:
object Parse {
def apply[T](f: String => T) = new Parse[T](f)
implicit val parseLong: Parse[Long] = Parse[Long](s => new StringOps(s).toLong)
implicit val parseDouble: Parse[Double] = Parse[Double](s => new StringOps(s).toDouble)
implicit val parseInt: Parse[Int] = Parse[Int](s => new StringOps(s).toInt)
}

Resources