-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
No way to type an object with null prototype #1108
Comments
Not sure why 'Bug' label as this is per spec (3.8.1 Apparent Type). The best workaround would be to create an object called interface NotAnObject {
toString: void;
toLocaleString: void;
valueOf: void;
// etc
}
var x: NotAnObject = Object.create(null); Technically you could then still write things like |
Okay, I've added the requirement of not wanting access to the property to compile either :) Null-prototype objects are only useful in a minor number of cases (maps, etc.) so I suppose there's no pressing reason to fix this. |
Marking it as a suggestion. this is a supported ES6 pattern that will need to support |
Seems this has changed in 1.4. If foo is typed as void, foo.toString is now an error. Playground
Is this deliberate? Is it going to stay like this and can I use this feature? |
Pointed out by @fdecampredon on IRC - by changing @RyanCavanaugh 's example to a class and making the members private, one can make accessing the property also an error. class NullProto {
private toString: any;
/* other Object.prototype members */
}
var foo: NullProto = Object.create(null);
var bar = foo.toString; // error - Property 'toString' is private Pointed out by @spion on IRC - this type still has a usable constructor property, so that needs to be eclipsed as well (based on https://gist.github.com/spion/65e1f98b168be67daefe): class NullProto {
private "constructor": NullProto;
private toString: any;
private toLocaleString: any;
private valueOf: any;
private hasOwnProperty: any;
private isPrototypeOf: any;
private propertyIsEnumerable: any;
} |
@mhegazy This is going to be a lot more useful and important as time goes by. Would it be possible to maybe make null prototypes the default? Any interface expected to have object prototype methods could easily do |
For example, we can't even start describing the node 6 headers object. This errors as interface Void {
hasOwnProperty: void
}
interface Headers extends Void {
[header: string]: string
} Edit: To clarify, using |
Well that would violate ES spec, but since ES does allow classes to extend interface Headers extends null {
[header: string]: string
} (Of course currently this doesn't compile, and TS doesn't treat |
@Arnavion Why would that violate ES spec? I was talking about using it in a type position, not a value position. interface X extends Object {} I do like |
Does this issue make more sense to prioritise now that TypeScript 2.0 has been released with |
The change is not simple. that is why we need a proposal on how to implement it. |
I don't understand the internals enough to submit anything, but would be happy to give it a go and write up a proposal. Should the proposal be a new issue? Is it alright if I just model if off an existing proposal (feel free to provide a link if there's a proposal you format that works well)? |
Not necessarly. if you feel the proposal would cover more issues, then it is fine to have it by itself. jsut up to you.
Sure. #10727 looks like a good one. This also would be helpful: https://github.com/Microsoft/TypeScript/wiki/Writing-Good-Design-Proposals We are not really looking for a formal proposal, just a formulation for what changes would be needed to implement, and what impact this has on other parts of the type system. |
This seems to be google's canonical result for |
@mhegazy @RyanCavanaugh what would you think about just changing namespace Object {
export type NullPrototype = Record<keyof Object, never>
}
interface ObjectConstructor {
create
<T extends object | null>
(proto: T):
T extends null ? Object.NullPrototype : T
} interface IDictLike<T> {[key: string]: T}
type DictLike = IDictLike<string> & Object.NullPrototype
const dictLike: DictLike = Object.create(null)
console.log(dictLike.key)
console.log(dictLike.toString())
// ^^^^^^^^^^^^^^^^^^^
// Cannot invoke an expression whose type lacks a call signature.
// Type 'never' has no compatible call signatures.
// Same type as Object.create(Array.prototype)
const arrayLike = Object.create([])
console.log(arrayLike.key)
// ^^^
// Property 'key' does not exist on type 'any[]'. Did you mean 'keys'?
console.log(arrayLike.toString()) |
@texastoland That solution doesn't work when trying to override a property that's in type NullPrototype = Record<keyof Object, never>;
const x: Record<'toString', string> & NullPrototype = Object.create(null);
x.toString = 'hello world';
// ^^^^^^^
// Type '"hello world"' is not assignable to type 'never'. ts(2322)
x.toString.toLowerCase();
// ^^^^^^^^^^^
// Property 'toLowerCase' does not exist on type 'never'. ts(2339) The above is actually a general property of intersection property types with no overlap: interface ObjectA {
prop: string;
}
interface ObjectB {
prop: number;
}
type Intersection = ObjectA & ObjectB;
const a: Intersection = null as any;
a.prop = 'hello world';
// ^^^
//Type '"hello world"' is not assignable to type 'never'. ts(2322)
a.prop = 0;
// ^^^
// Type '0' is not assignable to type 'never'.ts(2322) |
Since the resulting type is currently an explicit const x: null = Object.create(null) and it will compile just fine, even with checks for implicit These objects are used as dictionaries, in scenarios where arbitrary content may come from users (and you don't want Forbidding the keys found on the Object prototype makes it impossible to use these objects as arbitrary dicts, defeating the purpose. What if you made it an implicit any, that causes a type error unless properly typed? You'd also need something along this, if it was valid, to properly express the pattern: type Diff<T, U> = T extends U ? never : T;
type OProtoKey = keyof typeof Object.prototype
type PartialRecord<
T,
K extends string | number | symbol = string | number | symbol,
OP extends OProtoKey = OProtoKey
> = {
[P in K]?: P extends OProtoKey ? never : T;
} & {
[Q in OP]?: T;
}
const x:PartialRecord<string> = Object.create(null)
const o = x[4]
x.toString Edit: getting closer. Now Note that this is incomplete, as the Edit: Getting closer, but I still can't shadow all the keys: type Dict<
T,
U extends string | number | symbol = string | number | symbol,
K extends string | number | symbol = (string | number | symbol) & U,
OPK extends keyof Object = keyof Object & U
> = (
({[P in K]?: T;}) & ({ [Q in keyof Object]?: never; } | { [R in OPK]?: T; })
)
const x:Dict<string, "toString" | "a"> = Object.create(null);
x.toString; // string | undefined
x.a; // string | undefined
x.yy
//^^
// Property 'yy' does not exist on type '({ toString?: string | undefined; a?: string | undefined; } & { toString?: undefined; }) | ({ toString?: string | undefined; a?: string | undefined; } & { toString?: string | undefined; })'.
// Property 'yy' does not exist on type '{ toString?: string | undefined; a?: string | undefined; } & { toString?: undefined; }'.x.valueOf
// but...
x.valueOf // (method) Object.valueOf(): Object Edit: with type Dict<
T,
U extends string | number | symbol = string | number | symbol,
K extends string | number | symbol = (string | number | symbol) & U,
OPK extends keyof Object = keyof Object & U
> = (
({[P in K]?: T;}) & (
{ [Q in keyof Object]?: Q extends U ? T: never; } |
{ [R in keyof Object]?: R extends U ? T: never; }
)
)
|
Any progress on this issue ? I for one have a lot of functions which accept and return such bare, null-prototyped objects. I also often prefer to instantiate new empty objects with So currently, as a workaround I use custom type definitions like |
If i'm looking to create a large inheritance structure, I prefer them to start on a null-prototype, It inherently protects them from possible prototype polution on the Object.prototype and as I basically never need to support the legacy Object.prototype methods, there is no real downside. The lack of a proper Null object type in TS has made me use it less, and i dislike that. |
Any update on this ? I understand that it's not the most critical feature on Typescript but for some edge cases it's very frustrating not to have the correct type. It feels like Typescript forgot one type of javascript data. Of course the cases where you need a null-prototype object properly typed are rare but not nonexistent. For example if you are developing a third party library that uses null-prototype object to avoid any collision with names like "hasOwnProperty" or "toString". Or just for performance sake. And you want to indicate to your library's users that those feature are not accessible on those objects. But you can't so it can leads to some deep issues. I can imagine the challenge of introducing new types or keywords to Typescript. I think the simplest way to specify that an object doesn't have a prototype is to allow a definition of the prototype the same way Object.create() works so we could set it to null if needed (or a custom prototype which ties with the concept of inheritance). It could feel like 'passing an argument' to object types to define the prototype. The same way an interface can take arguments with dynamic typing to play with it. It could be something like that : const test: object<null> = Object.create(null);
interface Test<null> {
toto: string;
anotherObject?: object<SomeInterface>
}
function Test(a: { toto: string; }<null>) {
}
// tsx
return (
<Component<object<null>> />
) Hope this will one day make it's way to Typescript ... |
Any updates on this? I am constantly running this problem. It's been 9 years 😢 |
I made this, it's not pretty, but if you're ok with it not being pretty and a bit of a bodge... https://www.npmjs.com/package/null-proto-ts |
I feel like the only possible way to do this correctly is to model it the way it actually exists in JavaScript: specifically, that the "null-prototype object" is in fact the actual root of the type tree, not
In terms of syntax, I'd suggest just going with the ES6 standard and adopting
What does this need to move forward? Does it still need an official proposal created? |
Currently the base type of all objects in TS seems to be
{}
, which looks like an empty type but actually has members inherited from the Object object. This leaves no way to express a type with null prototype.I did accidentally discover that it's possible to type a variable as void:
This appears to suppress intellisense for members in VS and the playground, and it is assignable to
any
(Object.keys(foo)
compiles), so it would seem to be what I'm looking for. However this still letsfoo.toString()
compile somehow - I imagine the void type is getting elevated to Object automatically.Edit: Ryan's workaround to subtype Object and override all its members to void prevents those properties from being called as functions, but as he points out, that doesn't prevent them from being accessed.
The text was updated successfully, but these errors were encountered: