If your requirements or intentions differ for the usage of propertyOne
, propertyTwo
, and
property<strike>Three</strike>Third
, it's advisable to assign individual
generic type parameters to each. Otherwise, the compiler will default to inferring a single type argument as a
union of the three literal types, making it challenging to associate
propertyTwo
. Here is a refactored version:
function myFunction<
T,
K1 extends keyof T,
K2 extends keyof T,
K3 extends keyof T
>(
dataset: T[],
propertyOne: K1,
propertyTwo: K2,
propertyThird: K3
) {
}
To enforce that the property type of T
at key K2
must be string
, you can utilize a recursive constraint on T
by specifying that it should be assignable to Record<K2, string>
(utilizing the Record<K, V>
utility type representing a type with keys K
and values V
):
function myFunction<
T extends Record<K2, string>, // add constraint
K1 extends keyof T,
K2 extends keyof T,
K3 extends keyof T
>(
dataset: T[],
propertyOne: K1,
propertyTwo: K2,
propertyThird: K3
) {
dataset.forEach(x => x[propertyTwo].toUpperCase()); // okay
}
const dataset = [{ age: 23, name: 'josh', country: 'america' }]
const okay = myFunction(dataset, 'age', 'name', 'country'); // okay
const bad = myFunction(dataset, 'name', 'age', 'country'); // error!
// ------------------> ~~~~~~~
This implementation functions as intended. The compiler recognizes that each element in dataset
has a property valued as string
at the key propertyTwo
.
Callers will also receive an error if the argument passed for propertyTwo
does not correspond to a string
property within elements of dataset
. Bravo!
The only drawback is that callers may prefer to see the error on 'age'
rather than on dataset
and would appreciate sensible IntelliSense suggestions displaying only key names corresponding to string
properties. To achieve this, adjust the constraint on K2
.
Firstly, create a utility type KeysMatching<T, V>
which calculates the keys of T
where properties are assignable to
V</code. Since there isn't a built-in utility or mechanism operating this way (there's a request at <a href="https://github.com/microsoft/TypeScript/issues/48992" rel="nofollow noreferrer">microsoft/TypeScript#48992</a> for a native version recognized by the compiler), it needs to be constructed. Here's one possible method:</p>
<pre><code>type KeysMatching<T extends object, V> = keyof {
[K in keyof T as T[K] extends V ? K : never]: any
};
In this scenario, I'm employing key remapping in mapped types to map T
to a new type containing solely those keys K
where T[K] extends V
. If T
corresponds to
{age: number, name: string, country: string}
and
V
represents
string
, then the mapped type would be
{name: string, country: string}
. Subsequently, we retrieve its keys using
the keyof
operator, yielding
"name" | "country"
.
In place of K2 extends keyof T
, substitute
K2 extends KeysMatching<T, string>
:
function myFunction<
T extends Record<K2, string>,
K1 extends keyof T,
K2 extends KeysMatching<T, string>,
K3 extends keyof T
>(
dataset: T[],
propertyOne: K1,
propertyTwo: K2,
propertyThird: K3
) {
dataset.forEach(x => x[propertyTwo].toUpperCase());
}
const dataset = [{ age: 23, name: 'josh', country: 'america' }]
const okay = myFunction(dataset, 'age', 'name', 'country');
const bad = myFunction(dataset, 'name', 'age', 'country'); // error!
// -----------------------------------> ~~~~~
Now, the constraint is successfully enforced, and errors are triggered where desired!
You might find it redundant that T
is constrained to Record<K2, string>
and K2
is restricted to
KeysMatching<T, string></code (applied constraints are essentially identical). However, due to the compiler's inability to comprehend what <code>K2 extends KeysMatching<T, string>
implies within the
myFunction()
implementation... Therefore, removing the
T extends Record<K2, string>
constraint yields:
function myFunction<
T,
K1 extends keyof T,
K2 extends KeysMatching<T, string>,
K3 extends keyof T
>(
dataset: T[],
propertyOne: K1,
propertyTwo: K2,
propertyThird: K3
) {
dataset.forEach(x => x[propertyTwo].toUpperCase()); // error!
// -------------------------------> ~~~~~~~~~~~
// Property 'toUpperCase' does not exist on type 'T[K2]'
}
This underscores the existence of microsoft/TypeScript#48992; if a native KeysMatching
were present, theoretically, just
K2 extends KeysMatching<T, string></code could be written, enabling the compiler to understand that <code>T[K2]
ought to be assignable to
string
. Nevertheless, since such functionality doesn't currently exist, maintaining the redundant constraint proves beneficial.
Playground link to code