Reputation: 114695
I'm trying to grok the new conditional types in typescript 2.8.
For example, I have have some objects with array properties which in my flow must have exactly one element and I want to get this value. Here is the code I thought should work, it correctly allows only relevant properties to be passed in but I can't figure out how to specify the return type. I get the following compilation error:
Type
number
cannot be used to index typePick<T, { [K in keyof T]: T[K] extends any[] ? K : never; }[keyof T]>[K]
.
And the types of n
and s
are deduced to be number|string
rather than number
for n
and string
for s
.
Code follows:
type ArrayProperties<T> = Pick<T, {
[K in keyof T]: T[K] extends Array<any>? K : never
}[keyof T]>;
const obj = {
a: 4,
n: [2],
s: ["plonk"]
};
// Compilation error on next line
function single<T, K extends keyof ArrayProperties<T>>(t: T, k: K): ArrayProperties<T>[K][number] {
const val = t[k];
if (!Array.isArray(val))
throw new Error(`Expected ${k} to be an array`);
if (val.length !== 1)
throw new Error(`Expected exactly one ${k}`);
return val[0];
}
const n = single(obj, "n"); // 'n' should be of type 'number'
const s = single(obj, "s"); // 's' should be of type 'string'
const a = single(obj, "a"); // Should fail to compile (OK)
Upvotes: 5
Views: 2403
Reputation: 327754
There are a few workarounds you can use to get this working.
To force the TypeScript compiler to look up a property when it can't verify that the property exists, you can intersect the key type with the known keys. Like this:
type ForceLookup<T, K> = T[K & keyof T]; // no error
So you can change
ArrayProperties<T>[K][number]
to
ForceLookup<ArrayProperties<T>[K],number>
Let's make sure that works:
type N = ForceLookup<ArrayProperties<typeof obj>["n"],number>; // number ✔️
type S = ForceLookup<ArrayProperties<typeof obj>["s"],number>; // string ✔️
n
and s
:The problem is that K
is not being inferred as a string literal. To hint that the compiler should infer a string literal when possible for a type parameter, you can add the constraint extends string
. It is not well-documented, but there are particular situations in which TypeScript infers literal types instead of widening to the more general type (so when 1
is inferred as 1
and not number
, or when 'a'
is inferred as 'a'
and not string
). The contraint keyof ArrayProperties<T>
apparently does not trigger this non-widening, so K
is widened to keyof ArrayProperties<T>
in all cases. Here's the workaround for K
:
K extends string & keyof ArrayProperties<T>
Let's see it all in action:
declare function single<T, K extends string & keyof ArrayProperties<T>>(
t: T, k: K): ForceLookup<ArrayProperties<T>[K],number>;
const n = single(obj, "n"); // number ✔️
const s = single(obj, "s"); // string ✔️
const a = single(obj, "a"); // still error ✔️
All done!
Well, there's a bit of simplification I would do here. ArrayProperties<T>[K]
can be reduced to T[K]
, for any K
that you can actually use. So you get:
declare function single<T, K extends string & keyof ArrayProperties<T>>(
t: T, k: K): ForceLookup<T[K],number>;
All done for real now.
Hope that helps. Good luck!
Upvotes: 5