How do I type a prop
function that returns a default type when the key doesn't exist on the object o
?
type Prop = <K, O extends {}>(k: K, o: O) =>
K extends keyof O
? O[K]
: 'Nah';
/*
Argument of type 'K' is not assignable to parameter of type 'string |
number | symbol'.
Type 'K' is not assignable to type 'symbol'.
*/
const p: Prop = (k, o) => o.hasOwnProperty(k) ? o[k] : 'Nah';
p('non-existence-property', { n: 1 });
/*
Type '"Nah" | O[K]' is not assignable to type 'K extends keyof O ? O[K] : "Nah"'.
Type '"Nah"' is not assignable to type 'K extends keyof O ? O[K] : "Nah"'.
*/
const p1 = <K, O extends {}>(k: K, o: O): K extends keyof O ? O[K] : 'Nah' =>
o.hasOwnProperty(k) ? o[k] : 'Nah';
First, to aid in type inference when calling p()
, let's alter the definition of Prop
slightly:
type Prop = <K extends keyof any, O extends {}>(k: K, o: O) =>
K extends keyof O
? O[K]
: 'Nah';
I've just restricted K
to be string | number | symbol
, which is probably what you meant anyway, and has the benefit that functions of type Prop
will tend to infer K
to be string literals instead of string
.
The main problem you're having is that it's not really possible for the compiler to verify if a type is assignable to an unresolved conditional type (T extends U ? X : Y
when T
or U
depend on unspecified/uninferred generic type parameters). Generic functions returning conditional types are meant to make caller's lives easier; the implementer is more or less stuck using type assertions or the like to appease the compiler:
const p: Prop = (k: any, o: any) => o.hasOwnProperty(k) ? o[k] : 'Nah'; // no error
The annotation of k
and o
as any
allows us to write our own implementation without the compiler complaining. Of course, this is not type-safe, and we must be very careful not to lie to the compiler. Which, technically, we have:
// missing optional keys
const val: { a?: number } = (1 > 2) ? { a: 1 } : {};
const oops1 = p("a", val); // definitely "Nah" at runtime,
// but number | undefined at compile time, oops!
// subtypes with unknown extra keys
interface Animal { limbs: number; }
interface Cat extends Animal { lives: number; }
const cat: Cat = { limbs: 4, lives: 9 };
const animal: Animal = cat;
const oops2 = p("lives", animal); // definitely number at runtime,
// but "Nah" at compile time, oops!
// prototype properties
const regex = /hey/;
const oops3 = p("exec", regex); // definitely "Nah" at runtime,
// but function at compile time, oops!
These are all situations where your assumption that p
implements Prop
is shown to be incorrect. There are probably others, too. Only you know if your use cases are such where these situations don't matter. Maybe they don't matter, but you should be aware of it.
Anyway, I hope that helps you. Good luck!