A few findings from experimenting with isolated declarations:
Exported variables will need an explicit type annotation (✅ implemented).
export const c1: 1 = 1; // ✔ ok, is explicit
export const c2 = 1 + c1 // ❌, no explicit annotation
export let l1: number = 1; // ✔ ok, is explicit
export let l2 = 1 + c1 // ❌, no explicit annotation
export var v1: number = 1; // ✔ ok, is explicit
export var v2 = 1 + c1 // ❌, no explicit annotation
⚠ not implemented
One possible exception to this are const
declarations that have number
, string
, or boolean
literals assigned to them. These currently use export declare const x = 1;
in the declarations, and this behavior could be preserved even in isolated declarations
export const c1 = 1; // could be allowed ❓
export let l1 = 1; // ❌ let declarations should have annotations, even if they are assign literals
export var l1 = 1; // ❌ var declarations should have annotations, even if they are assign literals
⚠ not implemented
It is a common pattern to have a variable that holds a function. If the function type is fully specified (as detailed lower), an external tool could in principle move the type annotation from the function type to the variable.
//src.ts
export let fn = (param: string): string => param;
//src.d.ts
export declare let fn: (param: string) => string;
⚠ not implemented
If a variable is initialized with a class expression, an external tool could in principle, if the type of the class is fully specified as detailed below, move the type information from the class expression to the variable.
//src.ts
export let cls = class {
method(): void {}
}
//src.d.ts
export declare let cls: {
new (): {
method(): void;
};
};
All exported functions will require explicit type annotations on the return type (✅ implemented)
Possible exceptions:
- Functions that do not have a
return
statement or havereturn
without value, could be trivially inferred tovoid
without type information. (⚠ not implemented) - Functions that only return literal values, could also be trivially inferred to the base type of the literal (or union of them).(⚠ not implemented)
While in both of these possible exception cases we could infer the return type just syntactically based on what is in the file, it might be confusing to require the annotation in all but these very narrow cases. As a first iteration it might be best to always require the annotation
❕ A suggestion already exists that adds the inferred return type.
Currently typescript allows for properties to be added to functions. So this is valid TypeScript code:
// src.ts
export function test(): void {
}
test.prop = 10;
// src.d.ts
export declare function test(): void;
export declare namespace test {
var prop: number;
}
Since this will use the type of the assigned expression to type the property, and since there is no place to put an annotation in an assignment expression, this kind of expanding function type will be forbidden under isolatedDeclaration
(✅ implemented). Users will need to go back to specifying explicit namespaces
for the extra function properties.
export function test(): void {
}
declare namespace test {
var prop: number
}
test.prop = 10;
❓ We could consider allowing the addition of properties where the an assertion is used in the assignment as a sort of type annotation, although the semantics of that are obviously different (test.prop = 10 as number
)
Private fields (both ES private and TS private ) will not require annotations as these don't get preserved in declarations anyway.
Public and protected fields will require explicit annotations (✅ implemented).
Similarly as for const
declarations, readonly
declarations that have literals assigned to them could be exempt from the annotation requirement as they use assignment syntax in the declaration currently.(⚠ not implemented)
// index.ts
export class Base {
readonly x = 10;
}
// index.d.ts
export declare class Base {
readonly x = 10;
}
Exemptions for field initialized with functions or classes could be considered as for variable.(⚠ not implemented)
Private fields (both ES private and TS private ) will not require any extra annotations.
Public and protected methods will require explicit annotations on the return type (✅ implemented).
Currently is is sufficient to specify the type on only one of the get
or the set
accessors, with TypeScript inferring the type for the other. As long as there is an annotation on at least one of then, any tool that wants to independently emit declarations should be able to copy it from there, so this behavior is compatible with isolatedDeclarations
(✅ implemented)
export class A1 {
// ❌ No annotation should error under isolatedDeclarations
get x() { return "" }
}
export class A2 {
// ❌ No annotation should error under isolatedDeclarations
set x(value) { }
}
export class A3 {
// ❌ No annotation should error under isolatedDeclarations
get x() { return "" }
set x(value) { }
}
export class A4 {
// ✔ Annotation can be retrieved from other accessor
get x() { return "" }
set x(value: string) { }
}
export class A5 {
get x(): string { return "" }
// ✔ Annotation can be retrieved from other accessor
set x(value) { }
}
When declaration emit encounters and expression in the extends
clause of a type, it will create an extra variable in the declaration file, type it as the type of the extends
expression and use that as the base type in the class declaration.
// src.ts
class Base { baseMethod() { }}
function id<T extends new (...a: any[]) => any>(cls: T) {
return class extends cls {
mixin(): void;
}
}
export class Derived extends id(Base) {
}
export class DerivedInline extends class Based { baseMethod(){} } {
}
//src.d.ts
declare class Base {
baseMethod(): void;
}
declare const Derived_base: {
new (...a: any[]):
mixin(): void;
};
} & typeof Base;
export declare class Derived extends Derived_base {
}
declare const DerivedInline_base: {
new (): {
baseMethod(): void;
};
};
export declare class DerivedInline extends DerivedInline_base {
}
This relies heavily on the type checker to get the type of the extends
expression. As such it can't be supported under isolatedDeclaration
(✅ implemented). Also since there is no good place to put an explicit type annotation for the extends clause, the only workaround is to do the same as declaration emit currently does and separate out the extends expression into a variable and put a type annotation on the variable.
class Base { baseMethod() { }}
function id<T extends new (...a: any[]) => { } >(cls: T) {
return class extends cls { mixin() { } }
}
const Derived_base: {
new (...a: any[]): {
mixin(): void;
};
} & typeof Base = id(Base)
export class Derived extends Derived_base {
}
const DerivedInline_base:{
new (): {
baseMethod(): void;
};
} = class Base { baseMethod(){} }
export class DerivedInline extends DerivedInline_base {
}
❓ A suggestion could be provided to do this automatically.
Const enums are currently forbidden under isolatedModules
, under isolatedDeclarations
we can allow them, but there might be a differences in the way emit works for them. Currently if a const enum is used in the definition of another const enum, the original value is resolved. This would not be possible for isolatedDeclarations
as the declaration of the enum might come from another file. We could instead not resolve the const value and keep the original assignment.
// src.ts
export const enum BaseEnum {
V = 1,
}
export const enum DerivedEnum {
V = BaseEnum.V,
}
// src.d.ts - currently
export const enum BaseEnum {
V = 1,
}
export const enum DerivedEnum {
V = 1,
}
// src.d.ts - proposed
export const enum BaseEnum {
V = 1,
}
export const enum DerivedEnum {
V = BaseEnum.V,
}
An external tool couldn't check that BaseEnum.V
is indeed a constant, so it could not emit the same error as TS, but the assumption is that tsc
will be used for type errors.
The POC is still under development, the current resolver
methods are still used. I am still currently evaluating if it is reasonable for an external tool to implement these resolver methods or if their usage is problematic.
Not sure what this does in declaration, still need to look at it.
if (isDeclaration(input)) {
if (isDeclarationAndNotVisible(input)) return;
if (hasDynamicName(input) && !resolver.isLateBound(getParseTreeNode(input) as Declaration)) {
return;
}
}
While this function comes from the resolver, it seems to be doing only basic syntactic work, so this seems ok to use. An external tool would need to re-implement this, although such an implementation seems straight forward.
Some module specifiers get rewritten during declaration emit, and these in turn use getSymbolOfExternalModuleSpecifier
and getExternalModuleNameFromDeclaration
from the resolver which both do a fair amount of work. This does not seem to be related to types necessarily, but it does seem to encode a lot of logic that an external tool would need to closely track.
❓ Is keeping module specifiers 'as is' an option?
Both these functions seems to be used to keep track of referenced imports.
❓ Maybe an external tool can just keep all imports in the declaration even if they are not used in the types ?
These functions are used only to issue errors, and their return values don't make it into the declaration output, so their usage is fine. An external tool would not give the same errors, but the assumption is that it tsc
would be used for getting errors anyway.
The functionality of this function would need to be duplicated by any external tools.