likern
likern

Reputation: 3964

Type guard which narrows generic type T to Pick<T, U>

I have a type guard, which sole purpose is to check the existence of property in object and that it has some value in it.

With type system I want to say this phrase to compiler, if type guard check succeeds:

This is an input object, if type guard succeeds then output is an object with lineHeight property

For exact object it looks like this:

type PickedLineHeight = Pick<TextStyle, "lineHeight">
type NonNullableLineHeight = Required<PickedLineHeight>

function hasLineHeight(style: object): style is NonNullableLineHeight {
    return (
        style.hasOwnProperty("lineHeight") &&
        (style as PickedLineHeight).lineHeight !== undefined
    )
}

How would I develop more generic version of function like hasProperty(style, prop)?

My attempt was:

function hasProperty<T extends { [key: string]: any}, U extends keyof T>(style: T, prop: U): style is Pick<T, U> {
    return (
        style.hasOwnProperty(prop) &&
        style[prop] !== undefined
    )
}

but I constantly get this error message, which I can't eliminate or understand

enter image description here

Upvotes: 1

Views: 225

Answers (2)

jcalz
jcalz

Reputation: 328758

I'd probably type hasProperty() this way:

function hasProperty<T extends object, K extends keyof T>(
    style: T,
    prop: K
): style is T & { [P in K]-?: Exclude<T[K], undefined> } {
    return style.hasOwnProperty(prop) && style[prop] !== undefined;
}

This should reflect that hasProperty() will verify that the property exists and is not undefined. The guarded type, T & { [P in K]-?: Exclude<T[K], undefined> } is assignable to T (which is what the intersection T & ... says) and has extra restrictions on the K-keyed property. Note that { [P in K]-?: Exclude<T[K], undefined> } could also be written Required<Record<K, Exclude<T[K], undefined>>, which might be more understandable. Let's make sure it behaves as expected:

interface Example {
    required: string;
    optional?: string;
    requiredButPossiblyUndefined: string | undefined;
    requiredButPossiblyNull: string | null;
}

function checkExample(ex: Example) {
    ex.required.toUpperCase(); // okay

    // hasProperty de-optionalizes optional properties
    ex.optional.toUpperCase(); // error, possibly undefined
    if (hasProperty(ex, "optional")) {
        ex.optional.toUpperCase(); // okay
    }

    // hasProperty removes undefined from list of possible values
    ex.requiredButPossiblyUndefined.toUpperCase(); // error, possibly undefined
    if (hasProperty(ex, "requiredButPossiblyUndefined")) {
        ex.requiredButPossiblyUndefined.toUpperCase(); // okay
    }

    // hasProperty doesn't do anything with null
    ex.requiredButPossiblyNull.toUpperCase(); // error, possibly null
    if (hasProperty(ex, "requiredButPossiblyNull")) {
        ex.requiredButPossiblyNull.toUpperCase(); // error, possibly null
    }
}

Looks good. Okay, hope that helps. Good luck!

Link to code

Upvotes: 1

likern
likern

Reputation: 3964

Accidentaly I came up to this soluton, which is for my opinion, is the same as which shows error.

But anyway, this is working without complains:

function hasProperty<T extends object>(style: T, prop: keyof T): style is Pick<T, typeof prop> {
    return (
        style.hasOwnProperty(prop) &&
        style[prop] !== undefined
    )
}

function hasFontSize(style: TextStyle) {
    return hasProperty(style, "fontSize")
}

function hasLineHeight(style: TextStyle) {
    return hasProperty(style, "lineHeight")
}

Upvotes: 0

Related Questions