type-fest
type-fest copied to clipboard
Proposal: `PlainObject`
Motivation/Use-cases
If you want to model an object that can accept anything as value, you're tempted to use one of the following approach:
const myObject: { [props: string]: any } = { foo: 'bar' }
or
const myObject: Record<string, any> = { foo: 'bar' }
The problem being that both { [props: string]: any }
and Record<string, any>
accept functions and arrays, which I believe in 99% of cases is not what you were trying to model with your type. (Playground showcase here)
Proposal
We could construct a safer type based on the existing primitives of type-fest
:
import { Primitive } from 'type-fest'
/**
* Represents a POJO. Prevents from allowing arrays and functions
*/
export type PlainObject = {
[x: string]: Primitive | object
}
Notes
- The fact that it's very easy to construct with the existing type-fest primitives might be a case for not adding it, open for discussion
Upvote & Fund
- We're using Polar.sh so you can upvote and help fund this issue.
- The funding will be given to active contributors.
- Thank you in advance for helping prioritize & fund our backlog.
Your suggestion does not prevent nested things. Also what is the issue with arrays? I don't see any issue for a POJO to have an array, is there any?
I think the JsonObject
provided by type-fest is probably what you want.
I don't see any issue for a POJO to have an array, is there any?
The point here is not to prevent a POJO to have an array, the point is to prevent a POJO to be an array.
I think the JsonObject provided by type-fest is probably what you want.
JsonObject
is not what I want as it doesn't allow classes and functions as values.
const obj: JsonObject = {
a: () => {} // type error
}
Put differently, the PlainObject
type achieves the same as what lodash.isPlainObject does, but at the type level.
_.isPlainObject(new Foo); // => false
const obj: PlainObject = new Foo // type error
_.isPlainObject([1, 2, 3]); // => false
const obj: PlainObject = [1, 2, 3] // type error
_.isPlainObject({ 'x': 0, 'y': 0 }); // => true
const obj: PlainObject = { 'x': 0, 'y': 0 } // working
_.isPlainObject(Object.create(null)); // => true
const obj: PlainObject = Object.create(null) // working
Oh, wow, I get it now!! The fact that { [props: string]: any }
accepts () => {}
itself was so unexpected to me that I didn't even parse correctly what you were saying. Sorry about that.
// This is not an error! Wow!!
const foo: { [props: string]: any } = () => {}
This is so surprising to me that I would actually think this is a bug in TypeScript. What is the explanation for this?
Out of curiosity, I tried the following:
type Primitive = string | number | boolean | null | undefined | symbol | bigint
type Foo<T> = { [x: string]: T }
const foo1: Foo<any> = () => { } // OK (????????)
const foo2: Foo<unknown> = () => { } // Error
const foo3: Foo<never> = () => { } // Error
const foo4: Foo<Primitive> = () => { } // Error
const foo5: Foo<() => void> = () => { } // Error
Is any
the only type that does not give an error? Or is there another Foo<MagicalTypeHere>
that also does not give an error?
Oh yeah, I re-read the issue and saw how it wasn't really clear given my wording. Sorry about that too π
This is so surprising to me that I would actually think this is a bug in TypeScript. What is the explanation for this?
My theory is that { [props: string]: any }
gets "transformed" into an object
type, which allows both arrays and functions.
I didn't open a TS issue, but I found no question at all about this while googling so I ended up thinking it was more or less assumed correct by everyone π
Is any the only type that does not give an error? Or is there another Foo<MagicalTypeHere> that also does not give an error?
any
is the only one I found too.
This was found by an actual bug in one of our open-source project. The code looked like this, see if you can spot the problem π€¦ββοΈ:
function pluginHook(): { [props: string]: any } {
const internalHook = () => {
return {
some: { value: true }
}
}
return internalHook
}
Interesting, perhaps someone else can shed some light in this topic.
see if you can spot the problem
Forgot to call the function I guess :sweat_smile:
https://github.com/sindresorhus/type-fest/blob/eb96609c1b4db7afbf394e8b52fcf95b74bce159/source/internal.d.ts#L111-L116
Perhaps this could be of assistance to you?
However, it is important to note that itβs not absolutely safe in some situations. For instance, it still can't make correct judgment in this scenario:
let s = new String()
type T1 = IsPlainObject<typeof s>
// ^? true
There is another approach that might help you, but it has a drawback: It can't determine interface
type PlainObject = Record<string, unknown>
type T1 = [1, 2, 3] extends PlainObject ? true : false
// ^? false
type T2 = (() => string) extends PlainObject ? true : false
// ^? false
type T3 = String extends PlainObject ? true : false
// ^? false
type T4 = { a: 1 } extends PlainObject ? true : false
// ^? true
interface Obj {
a: 1
}
type T5 = Obj extends PlainObject ? true : false
// ^? false
As far as I know, there still seems to be no perfect way to implement isPlainObject in TypeScript currently. I have been seeking help for a long time too.