sindresorhus / ts-extras

Essential utilities for TypeScript projects
MIT License
587 stars 15 forks source link

Proposal: add type guard `objectHas` #48

Open tychenjiajun opened 2 years ago

tychenjiajun commented 2 years ago

In https://github.com/sindresorhus/ts-extras/pull/22 we added objectHasOwn. It's a type guard with return type to be object is (ObjectType & Record<Key, unknown>)

https://github.com/microsoft/TypeScript/issues/21732 suggested a similar type guard inOperator which has exactly the same return type of objectHasOwn. However, inOperator can assert property existence in prototypes while objectHasOwn can not.

I propose a new function objectHas here, which should have the same implementation as inOperator. The naming changed to objectHas to align with objectHasOwn.

Proposing implementation

export function objectHas<ObjectType, Key extends PropertyKey>(
    object: ObjectType,
    key: Key,
): object is (ObjectType & Record<Key, unknown>) {
    return key in object;
}

What's the difference between objectHasOwn and objectHas?

See https://tc39.es/ecma262/multipage/abstract-operations.html#sec-hasproperty and https://tc39.es/ecma262/multipage/abstract-operations.html#sec-hasownproperty

What's the difference between this issue and https://github.com/sindresorhus/ts-extras/issues/47?

https://github.com/sindresorhus/ts-extras/issues/47 proposing a new function keyIn that has the exact same function body but a different type definition and return type guard. They result in different type predicates. See https://github.com/microsoft/TypeScript/issues/43284#issuecomment-841748155

When to use keyIn, native in operator and objectHas

in operator

Native in operator is good at narrowing in union types. For example, https://github.com/sindresorhus/ts-extras/issues/30 can be resolved by just using the in operator.

const a: PromiseSettledResult<string> = { reason: '1', status: "rejected" }
if ('reason' in a) {
    // a is now PromiseRejectedResult
}

Playground

(But be careful that promiseSettledResults.filter(p => 'reason' in p) won't work as expected right without defining your own isRejected type guard.)

keyIn

keyIn should be used when you want to narrow the type of key to specific literals.

const a = 'foo';

const obj = {
    foo: 1
}

if (a in obj) {
    // a is literal type 'foo' now
}

Playground

objectHas

objectHas should be used when you want to safely check the existence of a property? I'm still confused about the use cases of this function.

tychenjiajun commented 2 years ago

Any thoughts? @younho9 @sindresorhus @jonahsnider

jonahsnider commented 2 years ago

Maybe it's just me, but, I don't see much utility in any of these functions other than objectHasOwn. I find that it can be very easy to make mistakes in the left-hand side of the in operator, especially when a property is renamed. I prefer to use discriminated unions whenever possible in my own code.

sindresorhus commented 2 years ago

I personally would not use objectHas either as I would not use foo in object. I only use objectHasOwn. Another concern with objectHas is that it's easy to type, so a user might reach for it without realizing objectHasOwn is better in most scenarios. So thinking more about this, if we were to add such a utility, it needs a more verbose name.