Typescript: Support for all type features in declaration files.

Created on 21 Dec 2019  路  22Comments  路  Source: microsoft/TypeScript

I'm on TS 3.7.3.

It seems like this issue should simply be focused on and fixed before continuing to add more and more features to the language. This is quite a problem!

Search terms

"typescript declaration file limitations"
"has or is using private name"
"exported class expression may not be private or protected"
"Property 'foo' of exported class expression may not be private or protected. ts(4094)"
"Return type of exported function has or is using private name 'foo'. ts(4060)"

Related issues (a small fraction of the results on Google) in no particular order:

TypeScript also recommends not aiming package.json "types" field at .ts files, but that is the only way to distribute things as mixin classes. See https://github.com/microsoft/TypeScript/issues/35744

Suggestion

Support for all type features in declaration files.

_Please._ 馃檹

Some work is being done, f.e. https://github.com/microsoft/TypeScript/issues/23127, but overall I feel that too much effort is being put onto language features while leaving declaration output behind in the dust.

This creates a very poor developer experience when people's working code... doesn't work (the moment they wish to publish it as a library and turn on declaration: true).

Please don't take this personally, but please realize how frustrating it must feel for someone to work on a project for months, only to end with these errors the moment they want to publish the code as a library with declaration: true, then having to abandon features and re-write their code, or try to point package.json "types" to .ts files only to face other issues.

I wish every new feature of the language came paired with working tests for equivalent declaration emit. Can this be made a requirement for every new language feature, just like unit tests are a requirement?

Use Case

Use any language feature, publish your code, then happily move along, without facing issues like

src/html/DeclarativeBase.ts:25:10 - error TS4094: Property 'onChildSlotChange' of exported class expression may not be private or protected.

25 function makeDeclarativeBase() {
            ~~~~~~~~~~~~~~~~~~~

src/html/WebComponent.ts:32:10 - error TS4060: Return type of exported function has or is using private name 'WebComponent'.

32 function WebComponentMixin<T extends Constructor<HTMLElement>>(Base: T) {
            ~~~~~~~~~~~~~~~~~

I worked hard for too many months to get the typings in the above code working, then I turned on "declaration": true and TypeScript said "today's not going to be your day, Joe, sorry!".

I hope you can realize how much of a pain this issue is, and realize the need to bring declaration emit to parity with language features before continuing and potentially making the problem worse.

Examples

The issue that sparked me to write this was https://github.com/microsoft/TypeScript/issues/35744.

In that issue there's an example of what implicit return types might look like:

export declare function AwesomeMixin<T extends Constructor>(Base: T) {
  type Foo = SomeOtherType<T> // only types allowed in here.

  // only special return statements allowed, or something.
  return declare class Awesome extends Base {
    method(): Foo
  }
}

We'd need solutions for other problems like the protected/private error above, etc.

Of course it'll take some imagination, but more importantly it will take some discipline: disallow new features without declaration parity.

Checklist

My suggestion meets these guidelines:

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

Most helpful comment

@RyanCavanaugh this seems to be a meta bug that aggregates all of the ways in which .d.ts cannot be generated for corresponding .ts.

I don't think anyone wants to reduce the capabilities of .ts in order to satisfy this issue. My read is that folks want .d.ts files to be able to faithfully represent as close to 100% of the type information contained in a corresponding .ts file as possible.

it's a request to add every type system feature needed to produce these representations, which isn't actionable

Personally I think it's actionable, but it's a very large task that should probably be broken down into sub-tasks.

With your insight, perhaps you could help us by suggesting existing sub-tasks that lead to a happier outcome for concerned users? It sounds like there is one already ("named type expressions") - is there a corresponding issue that can be referenced? After reading it, we might be able to work together to uncover the remaining 20% and file issues as needed 馃憤

All 22 comments

There were too many issues to reference, so I stopped. But I hope this shows how much of a time waster this problem _really is_.

Can you describe what the desired return type (to be used in the d.ts) of your example is?

@evmar This is a good description:

The return type is class Awesome extends Base { method(): Foo } where Base is determined from the generic type arg T that is inferred (or passed) at the call site of the AwesomeMixin function.

I believe declaration files could be made to support this.

Personally I find #30355 to be a sharp edge for anyone who hopes to publish a library that will be used by folks outside the TypeScript ecosystem. In general it seems unwholesome that declaration files are unable to express types with parity to .ts sources.

We have a self-imposed design constraint that we do not produce new named types in definition files. This is critical because it ensures that no one is taking a dependency on names that might change in the future!

If you have specific examples where you can show unambiguous code that should have some certain .d.ts emit that behaves under that constraint, please post them.

As a workaround today; we have a flag that will let you know when TS isn't able to produce a declaration file for your code.

Thank you for the feedback @RyanCavanaugh .

With respect, this design constraint seems to impose a double standard across consumers of .ts sources and .d.ts+.js sources.

Here is one of my strawperson use cases: vending mixins. It is probably one that you have heard before. In this basic case, I can work with .ts sources directly and everything is fine, but if I generate .d.ts I'm told that a private/protected member is not allowed in the class generated by the mixin:

playground

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

class Foo { 
    foo: boolean = true;
}

const BarMixin = <T extends Constructor<Foo>>(FooClass: T) => {
    class FooBar extends FooClass {
        protected bar: boolean = true;
    }

    return FooBar;
};

type BarInterface = InstanceType<ReturnType<typeof BarMixin>>;

AFAIK this is not producing a new named type (although perhaps I have an incorrect understanding of what name means in this context), but none-the-less falls into the category of disparity in how types are treated between .ts and .d.ts files. It's also notable that this issue arises only when leveraging API access modifiers in the scoped class.

I'm trying to publish a React component library. I have a utility hook that prevents me from setting "declaration": true, which is absolutely essential since this will be used in TypeScript projects:

The hook

import { MouseEvent, MouseEventHandler, useCallback } from 'react';

export default function useOnClick<T>(onClick?: MouseEventHandler<T>) {

  const handler = useCallback((event: MouseEvent<T>) => {
      // does some stuff
  }, []);

  return handler;
}

The error

src/hooks/useOnClick.ts:16:25 - error TS4060: Return type of exported function has or is using private name 'MouseEvent'.

16 export default function useOnClick<T>(onClick?: MouseEventHandler<T>) {
                           ~~~~~~~~~~

I'm confused as to why a type from React is preventing me from publishing types within my package.

Try writing down the return type of your function. (That is what the d.ts will contain, after all.)

@evmar If the return type is a class, you can not write the return type manually. class produces special types that can only be written with class syntax at the moment.

@RyanCavanaugh

We have a self-imposed design constraint that we do not produce new named types in definition files. This is critical because it ensures that no one is taking a dependency on names that might change in the future!

This obviously doesn't behave like regular .ts files, which is strange. Why have a certain constraint only in one environment (consuming .d.ts + .js files) and not the other (consuming .ts files)?

Try writing down the return type of your function. (That is what the d.ts will contain, after all.)

Weird, that actually worked... Shouldn't the inferred return type be emitted in the d.ts without the manual adding of return types?

@trusktr My point is, if you cannot find a way to write the return type manually, then the compiler doesn't have any way of generating the return type for you.

@liamross Is is possible the explicit type you wrote is different from the inferred type that the compiler wanted to generate?

My point is, if you cannot find a way to write the return type manually, then the compiler doesn't have any way of generating the return type for you.

Exactly! That's why I opened this feature request with the title "Support for all type features in declaration files.", so that functionality can be added (be it new syntax in declaration files, or something else).

I now see why I didn't follow you! What you're asking for is inexpressible in .ts files too, in that you can't write the type in a .ts or a .d.ts file. So the issue isn't really tied to declaration files, except to the extent that they require you to write types. For a (contrived) example of the difference, your code won't be usable by a documentation generator either (since it also will want to show types in the docs).

I am leaning toward declaring this unactionable due to lack of a concrete request. There are a wide variety of type system features; basically what's being requested here is either that we make the type system in implementation space actively worse to maintain the "all .ts can produce .d.ts" guarantee, which is just not the right way to make good software, or it's a request to add every type system feature needed to produce these representations, which isn't actionable. Named type expressions would probably go 80% of the way there, but what's the remaining 20%? There aren't a lot of specifics here and this isn't just a "go fix it" bug report.

@RyanCavanaugh this seems to be a meta bug that aggregates all of the ways in which .d.ts cannot be generated for corresponding .ts.

I don't think anyone wants to reduce the capabilities of .ts in order to satisfy this issue. My read is that folks want .d.ts files to be able to faithfully represent as close to 100% of the type information contained in a corresponding .ts file as possible.

it's a request to add every type system feature needed to produce these representations, which isn't actionable

Personally I think it's actionable, but it's a very large task that should probably be broken down into sub-tasks.

With your insight, perhaps you could help us by suggesting existing sub-tasks that lead to a happier outcome for concerned users? It sounds like there is one already ("named type expressions") - is there a corresponding issue that can be referenced? After reading it, we might be able to work together to uncover the remaining 20% and file issues as needed 馃憤

I don't know if it will be helpful, but I would like to offer this anecdote:

At my company (Google), TypeScript source _must_ be transpiled to .js+.d.ts as an intermediate step in the mainstream JS build pipeline. This means that any type-related feature that is not supported in a declaration file is unavailable to me and my TypeScript-authoring colleagues.

30979

It may be too late for this, or maybe it is a lot of effort in refactoring declaration code, but wouldn't it be easier if declaration files were simply source files stripped of all source, leaving behind only types?

For example, take this mixin:

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

export class Foo { 
    foo: boolean = true;
}

type MyNumber = number

let counter: MyNumber = 0

export const BarMixin = <T extends Constructor<Foo>>(FooClass: T) => {
    console.log('making FooBar class #', ++counter)

    return class FooBar extends FooClass {
        protected bar: boolean = true;
    }
};

it would be converted to something like

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

export class Foo { 
    foo: boolean/* = true .. implementation stripped .. */;
}

/* .. implementation stripped ..
type MyNumber = number

let counter: MyNumber = 0
*/

// no "declare" keyword here, for simplicity. We're already in a declaration `.d.ts` file, so it is implied.
export const BarMixin = <T extends Constructor<Foo>>(FooClass: T) => {
    // console.log('making FooBar class #', ++counter) .. implementation stripped ..

    // the "return" here is used only to define the return type. Simple!
    return class FooBar extends FooClass {
        protected bar: boolean/* = true .. implementation stripped .. */;
    }
};

You see how simple that is? The .d.ts file expresses _all_ of the types in the .ts file.

Are you concerned that code will rely on the private FooBar type? Isn't that.... the point?

It seems to me that this would greatly reduce the TypeScript implementation complexity, because now declaration types would rely on the same exact type system as source types.

Is #30979 the feature that will cover the case of the class types returned from a functions?

I think the implementation for that feature wouldn't be needed if declaration files were equivalent of source .ts files stripped of runtime implementation (and stripped of types that aren't used in publicly exposed interfaces). But it may be a huge effort to reollback and down that path.

Just imagine: for ever new .ts feature, a parallel declaration feature wouldn't need to be implemented and maintained. The implementation details are simply stripped.


As an alternative to the above idea of re-writing the declaration implementation, what if we add an option like declarationsUseSource, that simply copies .ts files to the output dir, instead of generating delcaration files?

It would be similar to pointing types in package.json to .ts files instead of .d.ts files, which works in many cases, except for issues like https://github.com/microsoft/TypeScript/issues/35744 which would need to be solved somehow.

I was curious about the (somewhat) new composite feature, and discovered that composite projects must have declaration set to true. This can cause all of the above issues in composite projects.

@RyanCavanaugh I have a large React UI Library written in TypeScript I'd really not like to export all types for props as there is a clear method for grabbing props of a component. Here's my example:

interface Props {
  children: React.ReactNode;
  disabled?: boolean;
  fullWidth?: boolean;
  intent?: 'primary' | 'secondary' | 'tertiary' | 'neutral' | 'destructive';
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
  size?: 'smallest' | 'small' | 'medium' | 'large' | 'largest';
}

export function UIButton(props: Props): JSX.Element {
  return (
    <button
      className={clsx('UIButton', {
        'UIButton--disabled': props.disabled,
        'UIButton--full-width': props.fullWidth,
        'UIButton--intent-destructive': props.intent === 'destructive',
        'UIButton--intent-neutral': props.intent === 'neutral',
        'UIButton--intent-primary': props.intent === 'primary',
        'UIButton--intent-secondary': props.intent === 'secondary',
        'UIButton--intent-tertiary': props.intent === 'tertiary',
        'UIButton--size-large': props.size === 'large',
        'UIButton--size-largest': props.size === 'largest',
        'UIButton--size-medium': props.size === 'medium',
        'UIButton--size-small': props.size === 'small',
        'UIButton--size-smallest': props.size === 'smallest',
      })}
      disabled={props.disabled}
      onClick={props.onClick}
    >
      {props.children}
    </button>
  );
}

and if people need access to the props, they can do so by using React.ComponentPropsWithoutRef<typeof UIButton>. This is a clear example I think for use of private interfaces because its still accessible, without having to deal with exporting the types explicitly.

The reason why this is an issue for me, is I have a single index file where I export * from './UIButton'; and every other component in my library, there would be a collision of types here, and I'd really rather not rename all my types to be prefixed with the component name.

The changes in https://github.com/microsoft/TypeScript/pull/32028 can lead to situations where code works perfectly fine in the editor, then when ready to publish a library, it all breaks when trying to make declaration files (due to the problems in the OP list of issues). (cc @sheetalkamat)

Was this page helpful?
0 / 5 - 0 ratings