Skip to content
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

Consider adding safe Record type #2

Open
Dimava opened this issue Jun 7, 2023 · 5 comments
Open

Consider adding safe Record type #2

Dimava opened this issue Jun 7, 2023 · 5 comments

Comments

@Dimava
Copy link

Dimava commented Jun 7, 2023

interface RecordConstructor {
	new <K extends PropertyKey = PropertyKey, T = unknown>(): Record<K, T>;
	new <K extends PropertyKey, T>(entries: [K, T][]): Record<K, T>;
	keys<O extends Record<any, any>>(o: O): keyof O[];
	values<O extends Record<any, any>>(o: O): O[keyof O];
	entries<O extends Record<any, any>>(o: O): { [K in keyof O]-?: [K, O[K]] }[keyof O];
	fromEntries<T extends new () => Record<any, any>, K extends PropertyKey, V>(this: T, o: [K, V][]): Record<K, V>;
}
export const Record: RecordConstructor = class Record<K extends string | number | symbol, T> {
        constructor(entries?: [K, T][]){ /* ... */ }
	static keys<O extends Record<any, any>>(o: O) { return Object.keys(o) }
	static values<O extends Record<any, any>>(o: O) { return Object.values(o) }
	static entries<O extends Record<any, any>>(o: O) { return Object.entries(o) }
	static fromEntries<T extends new () => Record<any, any>>(this: T, o: [any, any][]) {
		return Object.assign(new this(), Object.fromEntries(o))
	}
} as any; // or a function with null prototype to remove prototype entirely, or Object.setPrototypeOf(this, null) in constructor
Object.setPrototypeOf(Record.prototype, null);
delete Record.prototype.constructor
// maybe also export type Record<K extends string | number | symbol, T> = ...; if import would override global Record type

this makes user able to use proper records, without caring about o.constructor and o.__proto__ breaking the expected behaviour, and using Record.keys() on objects user believes to be records

@Sanshain
Copy link
Owner

As far as I understand, you want to match the type Record (object) of the typescript, which a priori does not have object methods (__proto__, constructor, valueOf etc) with the method of its initialization:

let a = new Record([['a', 1], ['b', 2]])    // object `a` has no `prototype` and `__proro__` in ts and runtime

instead of the usual:

let a = {a: 1, b: 1}                        // object `a` has no `prototype` and `__proro__` in ts, but has in runtime  

Did I get your point right?

@Dimava
Copy link
Author

Dimava commented Jun 11, 2023

I would say the original point was being able to use typed Record.keys / Record.entries for objects you know are records (to avoid typing Object.keys / Object.values) and then it's just unwrapped from there to its logical conclusion of having a complete RecordConstructor which can make nullproto records

@Sanshain
Copy link
Owner

Sanshain commented Jun 12, 2023

I would say the original point was being able to use typed Record.keys / Record.entries for objects you know are records (to avoid typing Object.keys / Object.values) and then it's just unwrapped from there to its logical conclusion of having a complete RecordConstructor which can make nullproto records

That's what I thought at first. But I was confused that in javascript the prototype of an object does not affect to the output of Object.keys/entries:

let tt = Object.setPrototypeOf({a: 1}, {b: 2})
console.log(Object.keys(tt))                                         // will be printed just ['a'] !
console.log(tt.a)                                                    // will be printed 1
console.log(tt.b)                                                    // will be printed 2

Thus, I did not see how the presence of __proto__ and constructor in runtime or typescript could break Object.keys behavior.

However, I see that the construction is to some extent more type-safe than the usual work with objects, it cannot protect against type covariance like this:

let a = new Record([['a', 1]])
let aa = {a: 1, b: 2, c: ''}
a = aa;

for (let k of Record.keys(a)){
    console.log(a[k].toFixed())         // runtime error!
}

Of course, this example is artificial, but possible. Such covariant behavior of types in the typescript caused the rejection of the Object.keys patch (as described in the readme), as the community perceived this as a potential vulnerability.

@Dimava
Copy link
Author

Dimava commented Jun 14, 2023

I think this can be bypassed by making Record having #private which will make it not not crossasignable

@Sanshain
Copy link
Owner

I think this can be bypassed by making Record having #private which will make it not not crossasignable

I've tried. But it didn't impact to cross assign ability behavior

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants