microsoft / microsoft/TypeScript

Private constructor type simplification produces incompatible type

オープン
#57,412 コメント 4 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

Bug Domain: Declaration Emit Help Wanted
主要言語
Go
スター
111k
フォーク
14.3k
平均マージ
1日 19時間
マージ済み PR(30日)
117

説明

🔎 Search Terms

private constructor incompatible parameter types removed

🕗 Version & Regression Information
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about private constructor type simplification
⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.4.0-beta#code/PTAEBUAsEsGdQMYBsCGt4BMD2BTWA7AcgBdQcAPOYgGlGlLlACsBXWUtnDUNHxLfADcc+aCNIB3FAE9QAMywAnUMWkAHaPgDmPfLORpYAOgCwAKAw4DinDwBG7RSgSkD6UAGEBjli5R2kWwBvc1Aw0DVFaEEUYlsEb2JFX2IlAAojTJRFLVgALlAWfABrfCwJfABtAF0ASgBucwBfc3MQUABBBATFDE0dVNAtERwnOO4jDCNiWFpiGHgkLCxi+ApnUlgUAFtbXi98Hz8AnHN-Rw3EVHcAcRGxrgOj4n9A0BCzcIiomLj+Q6SKXStXeLTMYPMFDUSlciXkRQQUFiXQQODUMw6eg813gAF5QGkDAAvIkFVRqHBYOSeRLJY6BEG4gB8oMaZihMP+7Hh+ERkGR3TRGL0d3wo1ijxxoHxhNQJLJ6kp1NF4vGT0B9JwjJZQSabPMbngAFlpAAFH4S7GGd6hcIAYki0QlADUUEgCnZloEUPg2V9Hb94rSgYo0ravmE1CwAtAEKAbChsPgkLIFFgCo5+tRwxGozG4wmkynQHZshmkv0cyCPhHwvM4EYHRa4q6kNKVMkcH7wmCwYbQCbzU64lb0AB1eiQAByWFN2R28Br9oDLrdHq9OB93cjzaDALpqVD1ZzX3rxibw5wrfbgK7Od7rTM7SgjEYEn5pHJeAQUXRoFgOB4DwpB2Cw0BINwxDQLstD0PAaiGP0ACE5hyAiSLECiQqwJi0ijrAaSDru+ENKh6Eflh6I4ViOKEWaxE4hO8wznOTjbLApFmG0YAvvAb4fioiqwD+0B-gBQHviIQz3BK3ByOBQHZLY0HQoo4xkEgAGSTYsGkHIKDgbAKFmGhvIYZRwrSCqDwYPhdFDoGJFsqZfICqiVG4dZsl2URl74Ux06zvO7ENEAA

💻 Code
// This class doesn't exist, it is just used as a convenient way for typing any class.
declare abstract class Constructable {
    private constructor(...args: unknown[]);
}

// According to generated .d.ts, this looks exact same as Constructable
abstract class GeneratedConstructable {
    private constructor() {}
}

export const funcThatAcceptsAnyClass = (clazz: typeof Constructable) => {};
export const funcThatAcceptsAnyGeneratedClass = (clazz: typeof GeneratedConstructable) => {};

class MyPrivateClass {
    #privateVal: boolean;
    private constructor(
        public readonly foo: string,
        public readonly bar: string
    ) {
        this.#privateVal = true;
    }
}
class MyPrivateClassWithNoParams {
    #privateVal: boolean;
    private constructor() {
        this.#privateVal = true;
    }
}

// This is what typescript sees at build time, its passing!
funcThatAcceptsAnyClass(MyPrivateClass);
funcThatAcceptsAnyClass(MyPrivateClassWithNoParams);

// This is what typescript sees when generated files are imported elsewhere, it fails!
funcThatAcceptsAnyGeneratedClass(MyPrivateClass);
funcThatAcceptsAnyGeneratedClass(MyPrivateClassWithNoParams);
🙁 Actual behavior

When a class contains a private constructor, the generated types file (.d.ts) strips the constructor down to the bare minimum, meaning any parameters are removed from the method.

This creates a type that is incompatible with the original type declaration.

Since it is only the result of generated code, it cannot be detected during build time. Instead it is caught when the types are referenced in a separate package.

🙂 Expected behavior

Either:

  1. Parameters are not stripped from private class constructors
    • preferred
    • Less likely to be breaking change
    • Would produce slightly larger type files
  2. Private constructors are treated as parameter-less during build time
    • Would not apply to "inside" the class (where private constructors can be legally called)
    • Would at least produce consistent type behavior
Additional information about the issue

I stumbled across this issue when trying to restrict the type of a value to anything with a class.

Essentially the use case is a map of Class -> Instance. Restricting to "is a class" is important for instanceof checks. (I understand JS can technically call new on any function, but its 2024 and I think it is fair to restrict "class" behavior to the class keyword)

The implementation was not actually responsible for doing the instantiation, so private or abstract constructors are totally acceptable.

I have various type checks in the package itself, that behave entirely as expected.
However when I started using this package in another, suddenly I am hit with:

Argument of type 'typeof MyPrivateClass' is not assignable to parameter of type 'typeof GeneratedConstructable'.
  Types of construct signatures are incompatible.
    Type 'new (foo: string, bar: string) => MyPrivateClass' is not assignable to type 'abstract new () => GeneratedConstructable'.
      Target signature provides too few arguments. Expected 2 or more, but got 0.

I would expect this kind of type behavior to either be caught during the original build time (second option in expected behavior) or to not implement this kind of constructor "simplifying" at all.

Perhaps it could be gated behind a compiler flag, because I can understand this is probably a fairly niche requirement. I understand in most cases, we use type comparisons against the instances, not the classes, and when using the classes a private constructor is an "internal" detail that usually does not expose it's functionality.

I would also accept an alternative to typeof Constructable.

The goal is to generate a type that effectively looks like private abstract new (...args: unknown[]) => any. However the private keyword is not allowed, and type matching fails on Cannot assign a 'private' constructor type to a 'public' constructor type

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

リンク先の TypeScript Playground にある private constructor の例をまず再現し、ソース宣言と生成された .d.ts 出力を比較します。private constructor の宣言生成パスを追跡し、その後、ビルド時の動作と生成される型の動作に一貫性を持たせ、パラメーター付きとパラメーターなしの private constructor を対象とする回帰テストを追加します。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
typescript
領域
compilers
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
停滞
明瞭さ
おおむね明確
初心者へのやさしさ
35/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。