-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Way of specifying non-enumerable properties #9726
Comments
Seems like kind of an edge case; I wouldn't want to have to think about this for every .d.ts file for the sake of a couple of APIs |
I'd like to see the |
Might be useful for making Would help fix this old issue: #18133 |
Even if it can't be on interfaces, it would still be useful for classes to have by default. |
Just want to take the opportunity to point out that with a |
This issue is often referenced as an example of unsoundness of typescript. I wouldn't say it's an edge case as switching from objects to maps, array to sets are popular refactorings and at the same time usage of spreads with arrays and objects are ubiquitous. Shouldn't we prioritize this? |
@RyanCavanaugh Any update on the Typescript team's thinking on this? Right now it's pretty painful to work around this. |
Hi everyone 👋
Just to share an example that is not an edge case. Our team has lost time debugging a problem caused by this, while Typescript was perfectly happy with the code: The problem was that spreading a I hope this helps to get a better perspective on this problem. Thanks! |
yeah this definitely doesn't seem like an edge case to me const foo: string[] = []
//error: Type '{}' is missing the following properties from type 'string[]': length, pop, push, concat, and 26 more.
const bar: string[] = {}
//no error
const baz: string[] = {...foo}
//no error, fails at runtime
baz.push("") |
An ability to make assertions on enumerability is essential. This is a source of many bugs, and if a little birdie told us that we are doing something suspicious here const v: IVector = new Vector();
const v2: IVector = { ...v };
v2.x += 3;// x is a non-enumerable setter / getter ...our users would be slightly less sad. I do not even insist on That is what we actually have. We expose plain-object API to users, but internally we use classes with getters, and sometimes people on the team can make this mistake. So if we were able to require either plainobjectness on interfaces or enumerability on properties it would help. |
Anyone could share at least a workaround? I am stuck since non-enumerable props should be dropped from the type when I do an object spread, but should be there until I do the spread. I have found no way to control this with the type system at compile time but it is happening on runtime. |
I don't think there is a workaround. It's just not possible with Typescript type system. |
Why is the issue of the type checker not recognizing the outer type (i.e. I could imagine that the Object.assign() Problem might cause the outer type problem in some tangled ways, but would the latter not still be much easier to fix than the former? The type checker would just have to look at the outer type (Array or Object literal) to decide that there is a type mismatch. The inner type is checked correctly anyways, why not check the outer type too? (Sorry, if I have a simplistic view about it, but currently I have no deeper insight about the type checking constraints there ...) |
You could convey non-enumerability by using a 'flavor' type. Of course this doesn't help with pre-existing library types or the results of language features (e.g. spread operator or instance types of classes), but you can strip away non-enumerable-flavored properties with a helper type ( /**** nonenum Flavor *****/
// Tag type ('flavor') to mark non-enumerable properties
type nonenum = {[nonEnumSym]?: typeof nonEnumSym};
/**** Helpers *****/
// Unique symbol for the above
declare const nonEnumSym: unique symbol;
// Utility type excludes non-enumerable members
type EnumerableOnly<T> = Omit<T,keyof FilterByType<T, nonenum>>;
// Utility type to pick only properties whose type matches a specified type/shape
type FilterByType<T, Value> = {
[P in keyof T as T[P] extends Value | undefined ? P : never]: T[P]
}
/**** Usage *****/
// Includes enumerable and non-enumerable types
interface NonEnumExample {
x: number;
y: number;
name: nonenum & string;
format: nonenum & (() => string);
}
type WithoutNonenums = EnumerableOnly<NonEnumExample>; Complete example: Playground link (includes an Object.assign facsimile that strips non-enumerable properties) |
How would you construct the proper output type without
I think an
The main way is to assert this manually, which is obviously painful and errorprone. For a more robust solution, see @snarfblam's reply as well as my addition below 👇👇
The answer to this lies in the nature of TypeScript's type system. TS does not employ nominal types but matches structurally only. See this trivial example: type User = { id: number }
type Media = { id: number }
const user : User = { id: 1 }
const media : Media = user // works fine Using nominal types, line 4 would have an error, but it doesn't because the shape/structure fits. I you come from a system with nominal types, you may expect stronger guarantees, but this makes a lot of sense in the context of TypeScript being JavaScript with types and needing to compile to JavaScript with minimal transformation. See https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals for details.
Nice, gets pretty close. Unfortunately, the flavor sticks to the value, not the key. Therefore, at least one more operation would be required to enable pushing that data out in a “clean” state. Another utility like this would do it: type CleanNonEnumerables<T> = {
[P in keyof T]: T[P] extends nonenum & infer V ? V : T[P]
} With that, a cleaned type can be generated. type Cleaned = CleanNonEnumerables<NonEnumExample> |
Is this issue why you can spread whatever properties you like onto objects that don't support them? TypeScript has no problem with this, apparently: type Person = {
name: string;
};
type Pie = {
flavor: string;
};
let eli: Person = { name: "Eli" };
let applePie: Pie = { flavor: "Apple" };
const weirdMutant: Person = {
...eli,
...applePie,
};
console.log(weirdMutant) I'm used to doing lots of inline spreads in things like Redux reducers and was surprised TypeScript couldn't help me out here. EDIT: Ah, I think I'm looking for #12936 |
Object assign is defined like below in TS:
Though from MDN it is only copying members that are enumerable:
So if
U
above has non-enumerable members, TS will copy them anyway. This is slightly incorrect and unsafe.One issue I recently ran into was
I was assuming I was copying all members of the
Selection
object to{}
:Though it didn't copy any members at all, because the
Selection
object only contains non-enumerable members.Proposal
Mark properties as non-enumerable
And have an operator to get the only "enum side" of a type:
The text was updated successfully, but these errors were encountered: