Search code examples
typescriptmapped-types

TypeScript yet another `prop` function with non-existence key


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';

Solution

  • 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!