Open MichaelMitchell-at opened 2 months ago
This probably isn't possible as the constuctor for types don't have to have any relation to the returned type. i.e. If you have new Foo()
this doesn't mean the result is of type Foo
(or even that a type called Foo
even exists).
Like the following example couldn't work with isolatedDeclarations
(if Foo
was imported) as TypeScript wouldn't know that new Foo()
actually returns Bar
without looking into lib.ts
:
// lib.ts
export type Bar = {
prop: number;
};
export type FooConstructor = new () => Bar;
export const Foo: FooConstructor = function() { /* ... */ };
// main.ts
import { Foo } from "./lib.ts";
const f = new Foo();
Like the following example couldn't work with
isolatedDeclarations
(ifFoo
was imported) as TypeScript wouldn't know thatnew Foo()
actually returnsBar
without looking intolib.ts
:
My proposal already covers this:
if the type of
foo
is notFoo
, there is a typecheck error.
Since there is no type Foo
, then the type of foo
clearly can't be Foo
đ
It's worth noting that it's possible to get the type via InstanceType
.
For example:
const x = new Map<string, number>();
(x satisfies InstanceType<typeof Map<string, number>>)
There may be some edge cases but this could be the mechanical transform that allows the general case to work. TS can error in cases where it's not that simple (eg if there are complicated overloads or something).
This was discussed in our initial ID meetings but a fully-correct implementation is quite fraught. The real check you have to do is pretend that a const x: Foo
annotation exists, resolve that Foo
(it's in type space, not expression space), and see if the Foo
that resolves too is "the same" one that the constructor refers to. But even "same" is tricky; you could imagine something like
// joker.ts
export type Bar = {
a: string;
}
export type Foo = Bar;
export const Foo = {
new(): Foo;
}
where
import { Foo, Bar } from "./joker.js"
const f = new Foo();
where at
const f: Foo
Foo
has three meanings:
Foo
joker->Foo
joker->Foo->Bar
While it's true that const f: Foo = new Foo()
would mean the same thing, we'd be looking at this thinking "new Foo() returns Bar
, not Foo
" and erroring.
The design we settled on for isolatedDeclarations to start was that any error checking must not consult the checker, such that an external emitter could know when an annotation is needed/not needed without tsc.
Anything past that are "optimistic" checks where one would need to call tsc somewhere to know that the output is usable (along with defining new rules that say stuff like "new Map
means : Map
"). Changing this is definitely a change in strategy of the feature.
But the lack of inference on new
is definitely the most annoying of the bunch.
I wrote the above on my phone and the page didn't update with Ryan's comment until I sent it, oops âšī¸
and see if the
Foo
that resolves too is "the same" one that the constructor refers to
Does it need to be that strict? Could we settle for checking assignability? Like treat it as semantically equivalent to new Foo() satisfies Foo
(or upcast<Foo>(new Foo())
or new Foo() satisfies Foo as Foo
if we're being pedantic about the behavior of satisfies
)?
Anything past that are "optimistic" checks where one would need to call tsc somewhere to know that the output is usable
I think that's exactly right and I think having optimistic checks brings some needed ergonomics to using isolated declarations. I think https://github.com/microsoft/TypeScript/issues/58800 is in the same boat (please prioritize that one over this though I'd hope both make it into 5.7).
Could we settle for checking assignability?
You're talking about a different feature than isolatedDeclarations
at that point. Immediately we'd get a request for strictIsolatedDeclarations
which enforces that they'd be exactly identical.
@RyanCavanaugh wouldn't your tricky case be handled by InstanceOf<typeof Foo>
? playground
Would there be some pure syntax transform that could be done like that? Are there edge cases that it couldn't handle?
Are there edge cases that it couldn't handle?
Unfortunately overloads don't really work with conditional types so a simple case like:
type C = {
new(): Foo1;
new(x: number): Foo2;
};
// The type here is always Foo2
const o: InstanceoOf<C> = new C();
If the overload-conditional types interation though could be fixed, then having a transform like would work:
type InstanceOf<Args extends ReadonlyArray<any>, C extends new(...args: Args) => any>
= C extends new(...args: Args) => infer R ? R : never;
const o1: InstanceOf<[], C> = new C();
const o2: InstanceOf<[number], C> = new C(3);
but this would require all parameters to be suitable for inference within isolated declarations.
đ Search Terms
isolated declarations constructor generic infer
â Viability Checklist
â Suggestion
Add a new flag or change the behavior of
--isolatedDeclarations
so that in the following code which is currently disallowed by isolated declarations:if the type of
foo
is notFoo
, there is a typecheck error. In return, the type offoo
will be emitted asFoo
.Similarly, if somehow in
foo
is not aFoo<A>
, there is a typecheck error.đ Motivating Example
Currently under isolated declarations, code must redundantly declare the type of a variable which is the result of a constructor call.
This setting would also eliminate https://github.com/microsoft/TypeScript/issues/59768
đģ Use Cases
^