Home > Back-end >  Read property of object as one of types
Read property of object as one of types

Time:12-13

Type definitions for some objects are a little wonky. I know the properties and I know the type but the compiler doesn't. I want to write a small function that extracts the property with the type that I expect, but throws an error if the type is wrong.

So I want to know if the "readProperty" function below can somehow tell the compiler that e.g. the extracted property is a number, because the developer wrote "number" when invoking the function

Is this possible?

function readProperty<T>(obj: T, key: keyof T, typeName: "string" | "number"): string | number {
    const value = obj[key]
    if (typeof value != typeName) {
        throw new Error(`Property ${key.toString()} must be a ${typeName}`)
    }
    return value
}

const someObj = {
    x: "123",
    y: 123,
}

const x = readProperty(someObj, "x", "number") // <-- compiler should "know" now that x is a number
const y = readProperty(someObj, "y", "string") // <-- compiler should "know that y is a string

CodePudding user response:

For this use case I don't think there's much use for making the function generic in the type of obj or the type of key. If the compiler actually knew enough about obj and key for such a generic call signature to be useful, you wouldn't need to do any further checking of the property type (or worse, the compiler would disagree with you about the type).

Instead, the important part is to get the call signature so that when you pass a value of string literal type "string" as typeName then the output of the function is of type string, and if you pass "number" then the output is of type number. The most straightforward way to represent a mapping between a string literal input type and an arbitrary output type is to use an object type like an interface, and then looking up the output property involves an indexed access type on the input string literal. Like this:

interface TypeofMap {
    string: string;
    number: number;
}

function readProperty<K extends keyof TypeofMap>(
    obj: object, key: PropertyKey, typeName: K
): TypeofMap[K] {
    const value = (obj as any)[key]
    if (typeof value != typeName) {
        throw new Error(`Property ${key.toString()} must be a ${typeName}`)
    }
    return value
}

So readProperty() is generic in K, the type of typeName which is constrained to be one of the keys of the TypeofMap interface... so either "string" or "number". And then the return type of the function is TypeofMap[K], the corresponding type string or number.

Note that the compiler cannot really verify that the implementation of readProperty conforms to the call signature. So I have asserted that obj is of the any type via (obj as any) to loosen the type checking inside the function body enough to prevent errors. That means you need to be careful that the implementation does the right thing. If you were to change, say, (typeof value != typeName) into (typeof value == typeName), the compiler would not notice or complain. So take care.

Anyway, let's see if it works from the caller's side:

const x = readProperty(someObj, "x", "number");
//    ^? const x: number
const y = readProperty(someObj, "y", "string");
//    ^? const y: string

Looks good!

Playground link to code

  • Related