typescript - Hur typar man `K extends keyof T` när T kan vara undefined?


Jag vill lägga till TypeScript-typer till denna JS-funktion:

function getProp(obj, prop) {
  if (!obj) {
    return undefined;
  }

  return obj[prop];
}

Om det inte fanns något krav på att hantera undefined, skulle detta fungera:

function getProp<T, P extends keyof T>(obj: T, prop: P): T[P] {
  // ...
}

Men med undefined fungerar inte detta naiva tillvägagångssätt med | undefined:

function getProp<T, P extends keyof T>(obj: T | undefined, prop: P): T[P] | undefined {
  if (!obj) {
    return undefined;
  }

  return obj[prop];
}

const obj1 = {
  id: 1,
  name: 'obj1',
};
const obj2 = undefined;

getProp(obj1, 'id');
getProp(obj2, 'id');
              ^^^ Argument of type '"id"' is not assignable to parameter of type 'never'.ts(2345)

Lekplats.

Vad är det bästa sättet att skriva detta på?

Svar #1

Jag tror att om jag skulle försöka gå all-in och göra så att getProp()-funktionen accepterar alla typer av obj-parametrar och sedan tillåta vilken nyckel som helst om obj kanske är undefined, skulle jag göra något liknande detta:

function getProp<T,
    P extends T extends any ? (undefined extends T ? keyof any : keyof T) : never
>(
    obj: T,
    prop: P
): T extends any ? (P extends keyof T ? T[P] : undefined) : never;
function getProp<T, P extends keyof T>(obj: T | undefined, prop: P): T[P] | undefined {
    if (typeof obj === "undefined") {
        return undefined;
    }

    return obj[prop];
}

Det är superkomplicerat men det har några möjligtvis önskvärda beteenden:

const obj1 = {
    id: 1,
    name: 'obj1',
};
const obj2 = undefined;
const items = [obj1];

getProp(items.find(i => i.id === 1), "id"); // number | undefined
getProp(obj1, "id"); // number
getProp(obj2, "who knows"); // undefined
getProp(Math.random() < 0.5 ? { a: 1 } : { a: "b" }, "a"); // string | number
getProp(Math.random() < 0.5 ? { a: 1 } : { b: 1 }, "z"); // error! "z" not "a" | "b"
getProp(Math.random() < 0.5 ? { a: 1 } : { b: 1 }, "a"); // number | undefined

Men för användningsfallet som du beskrev är du bara intresserad av att skicka in undefined i den mån obj kan vara T | undefined för någon specifik T, och du planerar aldrig att faktiskt kalla getProp(undefined, "something"). I så fall fungerar din "naiva" metod tillräckligt bra:

function getProp<T, P extends keyof T>(obj: T | undefined, prop: P): T[P] | undefined {
    if (typeof obj === "undefined") {
        return undefined;
    }

    return obj[prop];
}

Observera :

getProp(items.find(i => i.id === 1), "id"); // number | undefined
getProp(obj1, "id"); // number | undefined
getProp(obj2, "who knows"); // error!
getProp(Math.random() < 0.5 ? { a: 1 } : { a: "b" }, "a"); // string | number | undefined
getProp(Math.random() < 0.5 ? { a: 1 } : { b: 1 }, "z"); // error! "z" not "a" | "b"
getProp(Math.random() < 0.5 ? { a: 1 } : { b: 1 }, "a"); // number | undefined

Möjligen är det enda som är "dåligt" med detta att alla utskrifter av getProp() inkluderar undefined i deras typ, även när du anropar getProp(obj, "id"), ett känt definierat obj med ett känd-närvarande "id". Men det verkade vara vad du ville, så där har du det. Du svarade i princip på din egen fråga!

Så, eh, hoppas det hjälper. Lycka till!

Länk till kod

Svar #2

Jag tror att detta är vad du vill uppnå:

function getProp<O, P extends O extends undefined ? any : keyof O>(obj: O, prop: P)  {
  if (!obj) {
    return undefined;
  }

  return obj[prop];
}

const obj1 = {
  id: 1,
  name: 'obj1',
};
const obj2 = undefined;

getProp(obj1, 'id');
getProp(obj2, 'id');

Så om den första parametern är undefined sätter jag den andra typen som any. Jag typar inte returvärdet eftersom det infereras fint. Playground