Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript generates declaration d.ts file with `#private;` field

I have a library, written in Typescript, that is being distributed in 2 files: a compiled ECMAScript-2015 compatible Javascript file index.js and a Typescript declaration file index.d.ts. My goal is to make library accessible for both Javascript and Typescript developers (so that they have proper typings and autocomplete).

Lately I have upgraded to Typescript 3.9.7, and decided to refactor my code to use new private class fields declaration that utilizes # sigil instead of Typescript's private keyword.

To my surprise, my index.d.ts file become non-compatible with old Typescript versions due to including the #private; member on my classes.

Here is a comparison between old Typescript code generating old declaration file, and a new refactored Typescript code that generates a new non-compatible declaration file. The old code utilizing private keyword:

// index.ts
class MyClass {
    private field1: string = "foo";
    private field2: string = "bar";

    constructor() {
        console.log(this.field1, this.field2);
    }
}

// generated index.d.ts
declare class MyClass {
    private field1;
    private field2;
    constructor();
}

The new refactored code that uses # sigil to declare private names:

// index.ts
class MyClass {
    #field1: string = "foo";
    #field2: string = "bar";

    constructor() {
        console.log(this.#field1, this.#field2);
    }
}

// generated index.d.ts
declare class MyClass {
    #private;
    constructor();
}

Here is a page at Typescript playground that contains that sample code.

Now, if my customer that uses an old Typescript (let's say, version 3.7) will fetch my library (consisting of compiled index.js and declaration file index.d.ts, without the source index.ts file) and rely on index.d.ts types, they'll see the following error:

error TS1127: Invalid character.

The origin of that error is clear (the # sigil), so my questions are following:

  1. Is it okay if I postprocess my index.d.ts and remove the #private; line before I ship my library to customers, that don't have to know about implementation details? I can easily do that by using ttsc package, but I still worry that piece of typing information might be somehow important.
  2. What is the practical use for #private; line in index.d.ts? Why would a declaration file expose that a class utilizes private fields, if they can't be accessed anyway, and are implementation details?
  3. According to a topic in Typescript Github issues, this is the intended behavior so that classes with private fields retain their nominal typing behavior when emitted to a .d.ts file. Sadly, the meaning of that explanation slips away from me. Is there any extra documentation I can read to better understand the nominal typing behavior of Typescript?
like image 538
cinnamon Avatar asked Nov 06 '20 00:11

cinnamon


People also ask

What is D ts file in TypeScript?

d. ts files are declaration files that contain only type information. These files don't produce . js outputs; they are only used for typechecking. We'll learn more about how to write our own declaration files later.

Are D ts files automatically generated?

d. ts type are automatically created at build time.

What is declaration D ts?

Declaration files, if you're not familiar, are just files that describe the shape of an existing JavaScript codebase to TypeScript. By using declaration files (also called . d. ts files), you can avoid misusing libraries and get things like completions in your editor.

What is the difference between .ts and D ts?

ts allows a subset of TypeScript's features. A *. d. ts file is only allowed to contain TypeScript code that doesn't generate any JavaScript code in the output.


1 Answers

It makes the type "nominal" so that other types which expose the same public members are not seen as compatible with a type that has a private field. One case where this matters is if you have code like this:

class C {
    #foo = "hello";
    bar = 123;

    static log(instance: C) {
        console.log("foo = ", instance.#foo, " bar = ", instance.bar);
    }
}

I'm sure there are more examples, but this static method is just one that came to my head.

This C.log function requires an actual instance of the C class since it accesses a private-named instance field on the instance parameter. If the declaration emit doesn't reflect that the C type is nominal by indicating that it has an ES private field and instead only emits the public fields, the compiler will use structural type comparisons here and won't produce the expected type errors. For example, that declaration emit would allow dependent code to pass in { bar: 456 } to C.log without any compiler error.

like image 133
Joey Watts Avatar answered Sep 29 '22 06:09

Joey Watts