Skip to content

Make it possible to use private fields in type declarations #51489

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

Closed
5 tasks done
erik-kallen opened this issue Nov 11, 2022 · 7 comments
Closed
5 tasks done

Make it possible to use private fields in type declarations #51489

erik-kallen opened this issue Nov 11, 2022 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@erik-kallen
Copy link

erik-kallen commented Nov 11, 2022

Suggestion

🔍 Search Terms

private field type declaration

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Make it possible to use private fields in type declarations, like:

type MyType<T> = { a: number; #marker: T };
type GetMarkerType<T> = T extends { #marker: infer TResult } ? TResult : never;

Currently this gives an error saying that private fields can only be used inside classes. It is true that no code can be generated for private fields outside classes, but no code is generated when declaring a type.

📃 Motivating Example

Sometimes it is nice to add markers to a type that define how the type can be used (PhantomData<T> in Rust).

In my case, I automatically generate endpoint definition like

getUser = { method: 'GET', path: '/something' } as Endpoint<'GET', UserInfo>;

My definition of Endpoint currently looks like

type Endpoint<T> = { method: 'GET' | 'POST'; path: string; __markerBody: T }

This works, but the only thing that says that the body is fake is the ugly name. If I could instead write

type Endpoint<T> = { method: 'GET' | 'POST'; path: string; #markerBody: T }

no one could even possibly try to access the marker, and there would be no reason for the editor to ever show it.

This feature could also be used in libraries, where you might want opaque state objects to be passed between methods, like

type StateObject = { #marker: 'opaque' };
function createStateObject(initial: string): StateObject {
    return { initial } as StateObject;
}

function getInitialValue(obj: StateObject) {
    const realObject = obj as { initial: string };
    return realObject.initial;
}
@fatcerberus
Copy link

fatcerberus commented Nov 11, 2022

Since the stated use case is branded types, you probably want #202 or #21625.

@erik-kallen
Copy link
Author

#21625 seems unrelated. #202 seems to solve the use case with opaque types, but not the Endpoint case.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Nov 11, 2022
@RyanCavanaugh
Copy link
Member

To solve the Endpoint case, you'd use an intersection.

Private fields appearing in a structural position where they can never be legally accessed, and would have fairly confusing semantics (are they allowed to merge via intersection? how?) is a sort of "a screwdriver is a hammer if you hold it by the pointy end" solution -- it would work, but the right fix is clearly nominal types.

@fatcerberus
Copy link

a screwdriver is a hammer if you hold it by the pointy end

I've been looking for an analogy for this sort of thing forever; this is perfect.

@erik-kallen
Copy link
Author

I don't agree that it would be confusing, I expected it to work and got surprised by the error message. Private fields would work exactly like regular fields. As a matter of fact, they already kind of do, but you need to use declare class C { #x: 'x' } instead of just type T { #x: 'x' }, and they don't work when mapping types.

But sure, makes sense to not do this since it could have been solved by another issue that has been opened for 8 years with apparently no work on it. /s

@fatcerberus
Copy link

fatcerberus commented Nov 12, 2022

Private fields would work exactly like regular fields.

A non-class type with a private field would only be useful as an opaque type that you could cast something else to; you would never be able to directly construct an object that’s assignable to it, because you can’t write object literals with private fields in JS. I agree it would be confusing to have first-class support for an entire class of types for which no legal runtime values exist. Even if that were allowed, it’s semantically confusing because class A { #foo: number } is not assignable to any other class with the exact same signature, but here you’d be treating them as part of a structural check (”exactly like regular fields”)

If you’re instead proposing that object literals like { foo: 42 } be assignable to { foo: number, #marker: T }, maybe without a cast--with the private field only serving to differentiate it from other, similar types--then you might as well skip the ceremony and just ask for nominal types (e.g. unique { foo: number }), because that’s what that boils down to, and is much simpler to explain to someone reading the code than a type with phantom fields.

@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants