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

Support 'typeof class' types #41587

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open

Support 'typeof class' types #41587

wants to merge 9 commits into from

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Nov 18, 2020

With this PR we permit type queries (the typeof operator in a type position) to specify class expressions. For example:

type TC1 = typeof class {
    constructor(s: string);
    static n: number;
    s: string;
};

declare let C1: TC1;
C1.n;  // number
let c1 = new C1('hello');
c1.s;  // string

Effectively, typeof class {...} obtains the type of the contained class expression, i.e. the type of the class constructor function object produced by the class expression. The declaration of TC1 above is roughly equivalent to

type TC1 = {
    new (s: string): {
        s: string;
    };
    n: number;
};

but such declarations do not permit protected, private, and/or abstract members to be reflected. With typeof class that becomes possible.

Fixes #41581.

@treybrisbane
Copy link

@ahejlsberg How will type parameters interact with this feature? 👀

Is the following valid?

type FixedBox<T> = typeof class {
    constructor(readonly value: T);
};

declare const StringBox: FixedBox<string>;

If so, am I correct in assuming that StringBox would be of type new (value: string) => { readonly value: string; }?

Also, is the following valid?

type GenericBox = typeof class <T> {
    constructor(readonly value: T);
};

declare const Box: GenericBox;

In the (IMO unlikely) event that it is, would Box be of type new <T>(value: T) => { readonly value: T; }?

@treybrisbane
Copy link

Random additional question: Why is typeof actually required in this case? Why not instead just allow declaration-only class expressions in type positions?
E.g.

// Declaration-only; no method bodies allowed
type Cat = class {
  constructor(name: string);

  meow(): void;
};

@rbuckton has explored a very similar syntax previously in #36392 (comment) (see the last three lines of the example).

@rbuckton
Copy link
Member

@treybrisbane what's nice is that typeof class {} can just reuse the parse and nodes of ClassExpression and there's a lot we can reuse from existing typeof. The problem with type X = class {} is that we'd essentially need new Node subtypes to represent the class and a larger set of internal operations to handle class{} in any type position.

Copy link
Member

@rbuckton rbuckton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider changing how we manufacture types for declaration emit?

Given:

function f() {
  return class {};
}

We currently emit this for the declarations:

declare function f(): { 
  new (): {}
};

But it seems a better option to emit this instead:

declare function f(): typeof class {};

This would be especially beneficial for more complex class definitions.

@@ -38978,7 +38980,8 @@ namespace ts {
if (flags & ModifierFlags.Abstract) {
return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "abstract");
}
if (node.kind !== SyntaxKind.ClassDeclaration) {
// An abstract modifier is permitted on a class expression in a 'typeof abstract class {}' type
if (node.kind !== SyntaxKind.ClassDeclaration && node.kind !== SyntaxKind.ClassExpression) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't that also make it valid for a regular class expression? If not, could we consider allowing it in parse? It's never made sense to me that you can write this:

function f() {
  abstract class C {}
  return C;
}

But not this:

function f() {
  return abstract class C {};
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We certainly could allow it, but it would in effect be an expression syntax extension and we generally try to avoid those.

@@ -2860,10 +2860,24 @@ namespace ts {
return type;
}

function isStartOfTypeofClassExpression() {
return token() === SyntaxKind.ClassKeyword ||
token() === SyntaxKind.AbstractKeyword && lookAhead(() => nextToken() === SyntaxKind.ClassKeyword && !scanner.hasPrecedingLineBreak());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mention in my comment in checker.ts, is there a reason we don't permit abstract for class expressions? It seems like we should always do this, not just for typeof.

@ahejlsberg
Copy link
Member Author

ahejlsberg commented Nov 19, 2020

@treybrisbane Yes, both your FixedBox and GenericBox types are permitted and have the types you indicate. The tests associated with the PR actually include examples of both:

https://github.com/microsoft/TypeScript/pull/41587/files#diff-dfd7fded32d2fa4d27228d3c964f82231184d068fbf31958905ad6d42a195633

Look for the declarations of C5 and BoxFactory.

@ahejlsberg
Copy link
Member Author

Why is typeof actually required in this case? Why not instead just allow declaration-only class expressions in type positions?

The typeof xxx operator in a type position obtains the type of an expression. So far, xxx has been limited to just dotted names, but now we also permit class expressions. So

const C = class {};
type T = typeof C;

can be shortened to just

type T = typeof class {};

by simple substitution. If the typeof operator wasn't required, it would look an awful lot like an alternate form of class declaration and it would be less clear that it obtains the constructor type, not the instance type.

@ahejlsberg
Copy link
Member Author

Should we consider changing how we manufacture types for declaration emit?

Yes, but of course it brings about the classic compatibility dilemma, i.e. you wouldn't be able to generate .d.ts files with 4.2 that can be consumed by pre-4.2 versions of the compiler.

sheetalkamat added a commit to microsoft/TypeScript-TmLanguage that referenced this pull request Nov 20, 2020
@sandersn sandersn added this to Not started in PR Backlog Nov 30, 2020
}
}

declare function Printable2<T extends new (...args: any[]) => object>(Base: T): typeof class extends Base {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a difference between typeof class extends Base { and typeof class extends T { here?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry, T is the constructor type, Base is the instance type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear: T is the constructor type, Base is the constructor value.

@sandersn sandersn moved this from Not started to Needs review in PR Backlog Dec 1, 2020
@treybrisbane
Copy link

Any chance we could get a playground for this? Would love to do some testing. 🙂

@orta
Copy link
Contributor

orta commented Dec 15, 2020

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 15, 2020

Heya @orta, I've started to run the tarball bundle task on this PR at 7ee42c3. You can monitor the build here.

@wycats
Copy link

wycats commented Jan 5, 2021

I am extremely, extremely excited about this PR 🚀

@matthewadams
Copy link

Soon after this PR lands, I hope there's discussion of syntax sugar for mixins/traits, similar to how it's done in Dart using the with syntax: class Person with Nameable, Taxable, HasChildren<Person>, HasParents<Person> { ... } or even class Person extends Animal with Nameable, Taxable, HasChildren<Person>, HasParents<Person> { ... }!

@justinfagnani
Copy link

@matthewadams the TypeScript team doesn't want to create new non-type syntax, but I do have a proposal for mixins in JavaScript: https://github.com/justinfagnani/proposal-mixins

I haven't been able to work on it in a very long time, so if there are any potential co-champions out there, let me know.

@matthewadams
Copy link

matthewadams commented Feb 5, 2021

@matthewadams the TypeScript team doesn't want to create new non-type syntax, but I do have a proposal for mixins in JavaScript: https://github.com/justinfagnani/proposal-mixins

I haven't been able to work on it in a very long time, so if there are any potential co-champions out there, let me know.

@justinfagnani I'd consider myself to be a co-champion, and I know a couple of others that might be interested, too. I'm at matthew@matthewadams.me if you'd like to continue discussion.

@treybrisbane
Copy link

Since Orta's attempt to generate a playground for this failed, I tried testing this branch locally by installing it via yarn add --dev microsoft/TypeScript#7ee42c3021300ee2ee31db2edc2256fe41d6281e.

Attempting to compile

type BoxClass = typeof class {
  constructor(readonly value: unknown);
};

const BoxClass: BoxClass = class {
  constructor(readonly value: unknown) {}
};

results in

src/index.ts:1:24 - error TS2304: Cannot find name 'class'.

1 type BoxClass = typeof class {
                         ~~~~~

src/index.ts:1:30 - error TS1005: ';' expected.

1 type BoxClass = typeof class {
                               ~

src/index.ts:2:3 - error TS2304: Cannot find name 'constructor'.

2   constructor(readonly value: unknown);
    ~~~~~~~~~~~

src/index.ts:2:15 - error TS2304: Cannot find name 'readonly'.

2   constructor(readonly value: unknown);
                ~~~~~~~~

src/index.ts:2:24 - error TS1005: ',' expected.

2   constructor(readonly value: unknown);
                         ~~~~~

src/index.ts:2:24 - error TS2304: Cannot find name 'value'.

2   constructor(readonly value: unknown);
                         ~~~~~

src/index.ts:2:29 - error TS1005: ',' expected.

2   constructor(readonly value: unknown);
                              ~

src/index.ts:2:31 - error TS2693: 'unknown' only refers to a type, but is being used as a value here.

2   constructor(readonly value: unknown);
                                ~~~~~~~


Found 8 errors.

So it looks like the compiler isn't recognising the new typeof class syntax. At first I thought Yarn wasn't installing the correct version, but I can see the new baseline tests locally, so it seems to have installed correctly.

What am I doing wrong? 😅

@rajmondx
Copy link

rajmondx commented May 30, 2021

Ended up using something like this:

interface ConstructableFunctionTyped<T> extends NewableFunction {
  new(...args: any[]): T;
}

export type ConstructableFunction = ConstructableFunctionTyped<any>;
export type Class = ConstructableFunctionTyped<Object>;

From comment in #44337.

@canonic-epicure
Copy link

Any hope this PR can be merged any time soon? It seems to be the key piece for solving the well-known limitation of the declaration file generation (which is incompatible with class expressions).

@@ -157,7 +157,7 @@ namespace ts.refactor {
return true;
}
}
else if (isTypeQueryNode(node)) {
else if (isTypeQueryNode(node) && node.exprName.kind !== SyntaxKind.ClassExpression) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering out loud: it might be useful to make an isTypeQueryableNode helper or similar, since this is already used twice internally, and it'd be helpful for consumers?

@JoshuaKGoldberg
Copy link
Contributor

Pinging ... @DanielRosenwasser? - Is there anything blocking this PR beyond responding to review comments & resolving merge conflicts? +1 to the general enthusiasm for the feature landing. I'd be keen to contribute if it means this could land sooner.

cc @gr2m, who pointed me here as this would help a ton with the Oktokit APIs.

@DanielRosenwasser
Copy link
Member

Basically a lot of unsolved questions around nominality and compatibility if you check the notes at #41824.

Plus, what a lot of people really want something that allows you to encode everything from JS/TS expression land into TS declaration land, especially without generating intermediate local types, and this feature doesn't entirely do that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team Experiment A fork with an experimental idea which might not make it into master For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

typeof class