0

i have recently started writing in TypeScript, and i have encountered a problem that i can't seem to find an answer for.

Consider the code below:

interface testInterface {
  a: string;
  b: number;
  c?: number;
}

const testObject: testInterface = {
  a: "1",
  b: 2,
};

function selectorFunction<GenericKey extends keyof testInterface>(
  key: GenericKey
): Required<testInterface>[keyof testInterface] {
  if (testObject[key]) return testObject[key];
  throw new Error("Error");
}

A bit of explanation what i'm trying to achieve: the selectorFunction is supposed to return a value from the testObject, based on a key that it receives, while also having the correct type for that value. That's why the key property type is Generic.

On it's own it works fine, the problem starts when optional properties come into play, as the method return type is required, so that the method does not return any undefined values.

In theory it should be easy, just add an if to check if that property exists, and it should work, but typescript still gives me the following error:

  Type 'string | number | undefined' is not assignable to type 'string | number'.
    Type 'undefined' is not assignable to type 'string | number'.
      Type 'testInterface[GenericKey]' is not assignable to type 'number'.
        Type 'string | number | undefined' is not assignable to type 'number'.
          Type 'undefined' is not assignable to type 'number'.ts(2322)

1 Answers1

0

I think the following does what you want. Addionally, it gives you never as a return type whenever you input a key that would result in an error. (Playground link)

interface testInterface {
    a: "a"
    b: "b"
    c?: "c"
}


const testObject: testInterface = {
    a: "a",
    b: "b",
}


// https://stackoverflow.com/questions/50829805/get-keyof-non-optional-property-names-in-typescript
type RequiredKeys<T> = {
    [k in keyof T]-?: undefined extends T[k] ? never : k
}[keyof T]


type GetSafely<T, K extends keyof T> =
    K extends RequiredKeys<T>
        ? T[K]
        : never


// <---- For documentation/testing purposes only


    // type is "a" | "b" - as intended
    type ReqK = RequiredKeys<testInterface>
    // type is `"a"` - as intended
    type A = GetSafely<testInterface, "a">
    // type is `"b"` - as intended
    type B = GetSafely<testInterface, "b">
    // type is `never` - as intended
    type C = GetSafely<testInterface, "c">

// ---->


const getSafely = <T extends {}>(obj: T) =>
    <K extends keyof T>(key: K): GetSafely<T, K> => {
        if (obj[key] !== undefined) {
            return obj[key] as GetSafely<T, K>
        }

        throw new Error("Error");
    }

// key is correctly required to be in `"a" | "b" | "c"`
const selectorFunction = getSafely(testObject)


// <---- For documentation/testing purposes only

    // type is `"a"` - as intended
    const a = selectorFunction("a")
    // type is `"b"` - as intended
    const b = selectorFunction("b")
    // type is `never` - as intended
    const c = selectorFunction("c")

// ---->
Gerrit Begher
  • 363
  • 2
  • 14
  • It seems to be working when the parameter c does not exists in the object, but what about when it does exist? My goal was to get it safely when it exists in the object, but currently the type of c is always never. Also, i was trying to find a way to do this without type casting, as it could be considered to be not safe in some scenarios. – Hoodrobinrs Feb 20 '21 at 12:08