TypeScript generic array type predicate - javascript

I would like to have a generic type predicate that allows me to check the type of an object's property.
To illustrate this, I want to achieve the following:
const a: { [key in string]: string | string[]} = {
arrayProp: ["this", "is", "an", "array"],
otherProp: "this is a string"
};
Object.keys(a).forEach((prop: keyof typeof a) => {
if (isArrayProperty(a, prop)) {
// `a[prop]` is correctly detected as array
a[prop].push("another value")
}
})
I was expecting something like this to work
function isArrayProperty<T, P extends keyof T>(
obj: T,
prop: P,
): T[P] is Array<any> {
return Array.isArray(obj[prop]);
}
However TypeScript seems to have problems with the generics and is statement in the return type.
Additional notes
I know I could just pass the value to a function like Array.isArray(a["arrayProp"]) to get it work.
However, I want to go even one step further where I pass in a constructor and a property to see whether the property of an object is an array type:
type ClassConstr<T> = new (...props) => T
function isArrayProperty<T, P extends keyof T>(
obj: ClassConstr<T>,
prop: P,
): T[P] is Array<any> {
return // some magic happening here;
}
class A {
someProp: any;
}
const a = new A()
a = ["some", "array"];
if (isArrayProperty(A, "someProp") {
// `a.someProp` is correctly detected as array
a.someProp.push("new value");
}
The background is, that I have a separate schema definition for my classes that is only available at runtime.
These schema definitions then decide, whether a property is an array, a string, a date, ...
Therefore, I would like to have a function that allows me to still achieve type-safety in the components where I use these classes.

Consider this:
const a: { [key in string]: string | string[] } = {
arrayProp: ["this", "is", "an", "array"],
otherProp: "this is a string"
};
function isArrayProperty<T, P extends keyof T>(
obj: T,
prop: P,
): obj is T & Record<P, Array<any>> {
return Array.isArray(obj[prop]);
}
Object.keys(a).forEach((prop: keyof typeof a) => {
if (isArrayProperty(a, prop)) {
a[prop].push("another value")
}
})
Playground
You need to assure TS that obj in isArrayProperty is a record with prop P and value Array<any>

Related

type of dynamically accessed property in typescript

I have a function I want to reuse that is applicable on multiple properties of a custom type.
Something like this:
interface MyType {
prop1: string;
prop2: number;
}
type MyTypeKey = keyof MyType;
const testValue = (
obj: MyType,
property: MyTypeKey,
value: any, // <-- what type should go here?
) => {
if (obj[property] === value) {
console.log("do something");
}
}
testValue({prop1: "a", prop2: 1}, "prop1", "okay"); // should be okay
testValue({prop1: "a", prop2: 1}, "prop2", "oops"); // should be error
But I don't know how to do this since I don't know the type of the property value. How can I solve this?
(I am new to javascript/typescript, so forgive me for small typos and constructions and bad practices)
Use a generic argument to indicate the key. From that key you can look it up on MyType to get the associated value's type, and require that the value argument to the generic function be that type.
interface MyType {
prop1: string;
prop2: number;
}
const testValue = <K extends keyof MyType>(
obj: MyType,
property: K,
value: MyType[K],
) => {
if (obj[property] === value) {
// ...
}
}
declare const obj: MyType;
// OK
testValue(obj, 'prop2', 3);
// Not OK
testValue(obj, 'prop2', 'a');
For a more flexible function that can validate any sort of object, without hard-coding MyType into it, make another generic type for the object.
const testValue = <O extends object, K extends keyof O>(
obj: O,
property: K,
value: O[K],
) => {
if (obj[property] === value) {
// ...
}
}

Type backward referring in TypeScript

I need help with type declaration that contains a backward referring and I'll explain:
Given an interface Car like that:
interface Car {
name: string,
engine: Engine,
brand: Brand,
...
}
I need to create a type of the following structure:
[ 'engine', (engine1, engine2) => {...} ]
Where engine1 and engine2 will be the same type as Car['engine'] and without limitation of generality - I need an array of:
[(keyof T) as S, (s1: S, s2: S) => { ... }]
In words: an array that contains exactly two elements: the first - is a property of T and the second is a function that accepts arg1 and arg2 where their type is the same type as the type of T[the first item in the array]
Can someone help me to declare such a type?
This will do:
type MyType<T extends object> = { [S in keyof T]: [S, (s1: T[S], s2: T[S]) => any] }[keyof T];
const e: MyType<Car> = ['engine', (e1: Engine, e2: Engine) => { }];
Explanation (due to request in comment):
Just break it apart.
Consider type:
type MyType1<T extends object, S extends keyof T> = [S, (s1: T[S], s2: T[S]) => any];
This type seems pretty obvious. But has one disadvantage - you must type key name explicitly.
const e: MyType1<Car, 'engine'> = ['engine', (e1: Engine, e2: Engine) => { }];
So you make union of all MyType1 possibilities for each property key:
type MyType2<T extends object> = { [S in keyof T]: MyType1<T, S> }[keyof T];
The original type above was just these two steps in one ;).
There is maybe a better approach but this is what I have.
I can't see a way to do it with a "plain" type so my suggestion is to create a "factory" - basically a function to create this kind of array.
This gives more flexibility with creating types.
type Engine = {
model: number;
}
type Brand = {
name: string;
}
interface Car {
name: string;
engine: Engine;
brand: Brand;
}
interface ArrayFactory<T> {
<U extends keyof T>(prop: U): [U, (arg1: T[U], arg2: T[U]) => void];
}
const createArray: ArrayFactory<Car> = (prop) => [prop, (arg1, arg2) => console.log(arg1, arg2)];
const arr = createArray('engine');
console.log(arr[0] === 'engine1');
arr[1]({model: 1}, {model: 2})
playground

How to type generic merging function in typescript

I have a generic javascript function that I'm having trouble typing correctly in typescript. It takes an array of items. If those items are arrays, it flattens them into a single array. If they are objects, it merges them into one big object. otherwise it just returns the original array.
function combineResults(responses) {
if (responses.length === 0) {
return [];
}
if (Array.isArray(responses[0])) {
return responses.flat(1);
} else if (typeof responses[0] === 'object') {
return Object.assign({}, ...responses);
}
else {
return responses;
}
}
Is it possible to type this safely so that if you pass an array of arrays, your return type will be an array, and if you pass an array of objects, your return type will be an object. And if you pass an array of neither arrays or objects, your return type will be the original array type.
I'd be inclined to give this an overloaded type signature corresponding to each case. There are a few snags here: one is that you're checking just the first element of responses and assuming the rest of the elements are of the same type; but arrays can be heterogeneous. And since arrays in JS and TS are considered objects, the call signature for combineResults() might do weird things if you give it a heterogenous array like [[1, 2, 3], {a: 1}]. I don't know what you want to happen at runtime there, so I don't know what you want to see happen in the type signature. These are edge cases.
Another snag is that an array like [{a: 1}, {b: ""}] is considered in TypeScript to be of type Array<{a: number, b?: undefined} | {b: string, a?: undefined}>, and to turn that into {a: number, b: string} involves a lot of type-system hoop-jumping, including turning unions to intersections and filtering out undefined properties.
So, here goes:
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type Defined<T> = T extends any ? Pick<T, { [K in keyof T]-?: T[K] extends undefined ? never : K }[keyof T]> : never;
type Expand<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
function combineResults<T extends ReadonlyArray<any>>(responses: ReadonlyArray<T>): T[number][];
function combineResults<T extends object>(responses: ReadonlyArray<T>): Expand<UnionToIntersection<Defined<T>>>;
function combineResults<T extends ReadonlyArray<any>>(responses: T): T;
function combineResults(responses: readonly any[]) {
if (responses.length === 0) {
return [];
}
if (Array.isArray(responses[0])) {
return responses.flat(1);
} else if (typeof responses[0] === 'object') {
return Object.assign({}, ...responses);
}
else {
return responses;
}
}
The call signature should map arrays-of-arrays to an array, arrays-of-objects to an object, and any other array to itself. Let's test it:
const arrs = combineResults([[1, 2, 3], ["a", "b"]]); // (string | number)[]
const objs = combineResults([{ a: 1 }, { b: "hey" }]) // {a: number, b: string}
const nons = combineResults([1, 2, 3]); // number[]
Looks good, I think. Watch out for edge cases, though:
const hmm = combineResults([[1, 2, 3], { a: "" }])
/* const hmm: {
[x: number]: number;
a: string;
} ?!?!? */
You might want to tune those signatures to prevent heterogeneous arrays entirely. But that's another rabbit-hole I don't have time to go down right now.
Okay, hope that helps; good luck!
Playground link to code
Assuming the items are object, how about this:
function combineResults(responses: Array<object> | Array<Array<object>>): Array<object> | object {

How do I type the values of an object's fields with a specific subset of keys?

I have an object containing multiple types of values:
interface Foo {
id: number;
data: FooData;
username: string;
notes: string;
}
const foo: Foo = {
...
}
I have a function that requires a string, and am iterating through a specific list of fields in my object to use in that function, all of which contain string values:
const renderString = (value: string) => {
...
}
const fooKeys: keyof Foo = ["username", "notes"];
fooKeys.map((key) => {
renderString(foo[key])
}
The issue is that foo[key] can be a string, number, or FooData object, so I want to specify that foo[key] will ONLY be a field value of Foo with a key matching one in fooKeys, since they are all going to be string values.
We can assert foo[key] as string but that doesn't protect us against bad keys in fooKeys.
One option is to use a conditional type to only allow string properties since that appears to be the only ones allowed? For example:
type StringPropsOnly<T> = {
[Key in keyof T]: T[Key] extends string ? Key : never;
}[keyof T]
const fooKeys: StringPropsOnly<Foo>[] = ["username", "notes"];
fooKeys.map((key) => {
renderString(foo[key])
});
TypeScript Playground
This answer I found mentions as const:
const fooKeys = ["username", "notes"] as const;

Typescript make one parameter type depend on the other parameter

Say I have
interface Action<T> {
assignAction(key: keyof T, value: any): void;
}
Say T is of type
{
users: User[];
accounts: Account[];
}
Now, when calling assignAction, let's say I want to pass users. So this action is false because types don't match:
assignAction('users', accounts)
I don't know how to validate value, since its type depends on what you choose for key.
You should be able to add a generic to the assignAction function to help describe this relationship.
interface Action<T> {
assignAction<K extends keyof T>(key: K, value: T[K]): void;
}
Then once your Action instance is given a generic type it knows how to associate the relationship between key and value when you call assignAction
interface TypeA {
objectIdA: string
}
interface TypeB {
objectIdB: string
}
interface TypeC {
objectIdC: string
}
enum Param1 {
TypeA = "TypeA",
TypeBC = "TypeBC"
}
type INamespaceKeyMap =
& Record<Param1.TypeA, TypeA>
& Record<Param1.TypeBC, TypeB | TypeC>;
type INamespaceKeys<T extends Param1.TypeA | Param1.TypeBC> = INamespaceKeyMap[T];
function test<NS extends Param1, Key extends INamespaceKeys<NS>>(namespace: NS, key: Key) {
console.log("Called");
}
const objectA = {
objectIdA: "test"
} as TypeA;
const objectB = {
objectIdB: "test"
} as TypeB;
const objectC = {
objectIdC: "test"
} as TypeC;
test(Param1.TypeA, objectB) // not allowed
test(Param1.TypeA, objectA)
test(Param1.TypeBC, objectA) // not allowed
test(Param1.TypeBC, objectB)
test(Param1.TypeBC, objectC)

Categories