BACK
Featured image of post 【Typescript】5.0 版本發布

【Typescript】5.0 版本發布

此版本帶來了許多新功能,同時旨在使 TypeScript 更小、更簡單、更快。我們已經實施了新的裝飾器標準,添加了更好地支持 Node 和 bundlers 中的 ESM 項目的功能,為庫作者提供了控制泛型推理的新方法,擴展了我們的 JSDoc 功能,簡化了配置,並進行了許多其他改進。

原文
參考網站

此版本帶來了許多新功能,同時旨在使 TypeScript 更小、更簡單、更快。我們已經實施了新的裝飾器標準,添加了更好地支持 Node 和 bundlers 中的 ESM 項目的功能,為庫作者提供了控制泛型推理的新方法,擴展了我們的 JSDoc 功能,簡化了配置,並進行了許多其他改進。

如果您已經熟悉 TypeScript,也不用擔心!5.0 不是破壞性版本,你所知道的一切仍然適用。雖然 TypeScript 5.0 包括正確性更改和一些不常用選項的棄用,但我們相信大多數開發人員都會有與以前版本類似的升級體驗。

要開始使用 TypeScript 5.0,您可以通過 NuGet 獲取它,或者使用 npm 和下面的命令:

1
npm install -D typescript

你也可以按照指示在 Visual Studio Code 中使用新版本的 TypeScript 的說明進行操作。

以下 TypeScript 5.0 中新功能的快速列表!

  • 裝飾器
  • const 泛型參數
  • extends 支持多個配置文件
  • 所有的枚舉都是聯合枚舉
  • --moduleResolution 配置新增 bundler 支持
  • 自定義解析標誌
  • --verbatimModuleSyntax
  • 支持 export type *
  • JSDoc 支持 @satisfies
  • JSDoc 支持 @overload
  • 運行 tsc --build 可以傳入的新指令
  • 編輯器中不區分大小寫的導入排序
  • switch/case 語法補足
  • 速度、內存和包大小優化
  • 重大更改和棄用
  • 下一步是什麼?

自 Beta 和 RC 以來有什麼新功能?

beta 版 發布以來,TypeScript 5.0 有幾個顯著的變化。

自 TypeScript 5.0 Beta 以來,一個新區別是 TypeScript 允許將裝飾器放置在 exportexport default 之前或之後。這一變化反映了 TC39(ECMAScript/JavaScript 的標準機構)內部的討論和共識。

另一個是新的 bundler 模塊解析選項只能在 --module 選項設置為 esnext 時使用。這樣做是為了確保在輸入文件中寫入的 import 語句不會在捆綁器解析它們之前轉換為 require 調用,無論捆綁器或加載器是否遵從 TypeScript 的 module 選項。我們還在這些發布說明中提供了一些上下文,建議大多數庫作者堅持使用 node16 or nodenext

雖然 TypeScript 5.0 Beta 附帶了此功能,但我們沒有記錄我們在編輯器場景中支持不區分大小寫的導入排序的工作。這部分是因為用於自定義的 UX 仍在討論中,但默認情況下,TypeScript 現在應該可以更好地與您的其他工具一起使用。

自我們的 RC 以來,我們最顯著的變化是 TypeScript 5.0 現在在 package.json 中指定了 Node.js 的最低版本為 12.20。我們還發布了一篇關於 TypeScript 5.0 向 modules 遷移的文章,並提供了鏈接。

自 TypeScript 5.0 Beta 和 RC 發布以來,速度基準和包大小增量的具體數字也進行了調整,儘管噪音一直是運行過程中的一個因素。為了清晰起見,還對一些基準的名稱進行了調整,包大小的改進也被移至單獨的圖表中。


裝飾器

裝飾器是即將推出的 ECMAScript 功能,它允許我們以可重用的方式自定義類及其成員。

讓我們思考以下代碼:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const p = new Person("Ron");
p.greet();

greet 這裡很簡單,但讓我們想像它更複雜——也許它執行一些異步邏輯,它是遞歸的,它有副作用…等等。不管你想像的是哪種場景,假設你拋出了一些 console.log 調用來幫助調試 greet。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  greet() {
    console.log("LOG: Entering method.");
    console.log(`Hello, my name is ${this.name}.`);
    console.log("LOG: Exiting method.")
  }
}

這種模式相當普遍。如果有一種方法我們可以為每種方法做到這一點,那就太好了!

這就是裝飾器的用武之地。我們可以編寫一個 loggedMethod 的函數,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function loggedMethod(originalMethod: any, _context: any) {

  function replacementMethod(this: any, ...args: any[]) {
    console.log("LOG: Entering method.")
    const result = originalMethod.call(this, ...args);
    console.log("LOG: Exiting method.")
    return result;
  }

  return replacementMethod;
}
  1. 輸出 “Entering…”
  2. this 將其所有參數傳遞給原始方法
  3. 輸出 “Exiting…” 日誌
  4. 返回原始方法返回的任何內容

現在我們可以使用 loggedMethod 來裝飾方法 greet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  @loggedMethod
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}
1
2
3
4
5
// Output:
//
// LOG: Entering method.
// Hello, my name is Ron.
// LOG: Exiting method.

我們只是在 greet 上面使用了 loggedMethod 作為裝飾器,注意我們把它寫成了 @loggedMethod。當我們這樣做時,它會被 target 方法和 context 對象調用。因為 loggedMethod 返回了一個新函數,該函數替換了 greet

我們還沒有提到 loggedMethod 用第二個參數定義的。它被稱為 “上下文對象",它有一些關於如何聲明修飾方法的有用信息,比如它是 #private 成員還是靜態成員,或者方法的名稱是什麼。讓我們重寫 loggedMethod 以利用它並打印出被裝飾的方法的名稱。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);

  function replacementMethod(this: any, ...args: any[]) {
    console.log(`LOG: Entering method '${methodName}'.`)
    const result = originalMethod.call(this, ...args);
    console.log(`LOG: Exiting method '${methodName}'.`)
    return result;
  }

  return replacementMethod;
}

我們現在使用 context 參數,它是 loggedMethod 中第一個具有比 anyany[] 更嚴格的參數類型。

TypeScript 提供了一個名為 ClassMethodDecoratorContext 的類型,他對方法裝飾器所接收的上下文對象進行建模。

除了元數據之外,方法的上下文對象還有一個有用的函數,稱為 addInitializer。這是一種掛鉤到構造函數開頭的方法(如果我們使用 static,則掛鉤到類本身的初始化)。

例如:在 JavaScript 中,通常會編寫類似以下模式的內容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
    this.greet = this.greet.bind(this);
  }

  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

或者,greet 可以聲明為初始化為箭頭函數的屬性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
    this.greet = this.greet.bind(this);
  }

  greet = () => {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

編寫此代碼是為了確保在 greet 作為獨立函數調用或作為回調傳遞 this 時不會重新綁定。

1
2
3
4
const greet = new Person("Ron").greet;

// We don't want this to fail!
greet();

我們可以編寫一個裝飾器,使用 addInitializer 在構造函數中調用 bind

1
2
3
4
5
6
7
8
9
function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
  const methodName = context.name;
  if (context.private) {
    throw new Error(`'bound' cannot decorate private properties like ${methodName as string}.`);
  }
  context.addInitializer(function () {
    this[methodName] = this[methodName].bind(this);
  });
}

bound 不返回任何東西——所以當它裝飾一個方法時,它會保留原來的方法。相反,它將在任何其他字段初始化之前添加邏輯。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
 
  @bound
  @loggedMethod
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const p = new Person("Ron");
const greet = p.greet;

// Works!
greet();

請注意,我們堆疊了兩個裝飾器:@bound@loggedMethod。這些裝飾以**相反的順序**運行。即 @loggedMethod 裝飾原始方法 greet,@bound 裝飾 @loggedMethod 的結果。在此示例中,這並不重要,但如果您的裝飾器有副作用或期望特定順序,則可能會發生這種情況。

同樣值得注意的是:根據你喜歡代碼風格,可以將這些裝飾器放在同一行。

1
2
3
@bound @loggedMethod greet() {
  console.log(`Hello, my name is ${this.name}.`);
}

可能不太明顯的是,我們甚至可以創建返回裝飾器函數的函數。這使得定制最終的裝飾器成為可能。如果我們願意,我們可以讓 loggedMethod 返回一個裝飾器並自定義它記錄消息的方式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function loggedMethod(headMessage = "LOG:") {
  return function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext) {
    const methodName = String(context.name);

    function replacementMethod(this: any, ...args: any[]) {
      console.log(`${headMessage} Entering method '${methodName}'.`)
      const result = originalMethod.call(this, ...args);
      console.log(`${headMessage} Exiting method '${methodName}'.`)
      return result;
    }

    return replacementMethod;
  }
}

如果我們這樣做,我們必須在使用 loggedMethod 作為裝飾器之前調用它。然後我們可以傳入任何字符串作為輸出到控制台的日誌的前綴。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  @loggedMethod("")
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const p = new Person("Ron");
p.greet();

// Output:
//
// Entering method 'greet'.
// Hello, my name is Ron.
// Exiting method 'greet'.

裝飾器不僅僅可以用在方法上!它們可用於屬性/字段、getter、setter 和自動訪問器。甚至類本身也可以為子類化和註冊之類的事情進行裝飾。

要深入了解有關裝飾器的更多信息,您可以閱讀 Axel Rauschmayer 的詳盡摘要

有關涉及的更改的更多信息,您可以查看原始 pull request

與實驗性遺留裝飾器的差異

如果您已經使用 TypeScript 一段時間,您可能會意識到它多年來一直支持”實驗性“裝飾器。雖然這些實驗性裝飾器非常有用,但它們模擬了一個更舊版本的裝飾器提案,並且始終需要一個名為 --experimentalDecorators。任何在沒有此標誌的情況下嘗試在 TypeScript 中使用裝飾器都會提示錯誤消息。

--experimentalDecorators 在可預見的未來將繼續存在;然而,如果沒有這個標誌,裝飾器現在將成為所有新代碼的有效語法。在之外 --experimentalDecorators,它們將以不同方式進行類型檢查和釋放。類型檢查規則和emit 完全不同,雖然可以編寫裝飾器來支持舊的和新的裝飾器行為,但任何現有的裝飾器函數都不太可能這樣做。

這個新的裝飾器提案與 --emitDecoratorMetadata 不兼容,它不允許裝飾參數。未來的 ECMAScript 提案可能會幫助彌合這一差距。

最後一點:除了允許將裝飾器放在 export 關鍵字之前,裝飾器提案現在還提供了在 exportexport default 之後放置裝飾器的選項。唯一的例外是不允許混合使用這兩種樣式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// allowed
@register export default class Foo {
  // ...
}

// also allowed
export default @register class Bar {
  // ...
}

// error - before *and* after is not allowed
@before export @after class Bar {
  // ...
}

編寫類型良好的裝飾器

上面的 loggedMethodbound 裝飾器示例有意簡單化並省略了很多關於類型的細節。

鍵入裝飾器可能相當複雜。例如,上面的類型正確的版本 loggedMethod 可能看起來像這樣:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function loggedMethod<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  const methodName = String(context.name);

  function replacementMethod(this: This, ...args: Args): Return {
    console.log(`LOG: Entering method '${methodName}'.`)
    const result = target.call(this, ...args);
    console.log(`LOG: Exiting method '${methodName}'.`)
    return result;
  }

  return replacementMethod;
}

我們必須使用類型參數 ThisArgsReturn 分別定義 this 的類型、參數和原始方法的返回類型。

裝飾器函數定義的具體複雜程度取決於您要保證的內容。請記住,您的裝飾器將被使用的次數多於它們被編寫的次數,因此類型良好的版本通常更可取——但顯然需要與可讀性進行權衡,因此請盡量保持簡單。

將來會提供更多關於編寫裝飾器的文檔,但這篇文章應該有大量關於裝飾器機制的細節。


const 泛型參數

在推斷對象的類型時,TypeScript 通常會選擇一種通用的類型。例如,在本例中,names 的推斷類型是 string[]

1
2
3
4
5
6
7
type HasNames = { readonly names: string[] };
function getNamesExactly<T extends HasNames>(arg: T): T["names"] {
  return arg.names;
}

// Inferred type: string[]
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});

通常這樣做的目的是使突變成為可能。

但是,根據 getNamesExactly 的具體功能和用途,通常需要更具體的類型。

到目前為止,API 作者通常不得不在某些地方添加 as const 以實現所需的推理:

1
2
3
4
5
6
7
8
9
// The type we wanted:
//   readonly ["Alice", "Bob", "Eve"]
// The type we got:
//   string[]
const names1 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});

// Correctly gets what we wanted:
//   readonly ["Alice", "Bob", "Eve"]
const names2 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]} as const);

這可能很麻煩且容易忘記。在 TypeScript 5.0 中,您現在可以將 const 修飾符添加到類型參數聲明中,以使 const-like 推理成為默認值:

1
2
3
4
5
6
7
8
9
type HasNames = { names: readonly string[] };
function getNamesExactly<const T extends HasNames>(arg: T): T["names"] {
//                       ^^^^^
  return arg.names;
}

// Inferred type: readonly ["Alice", "Bob", "Eve"]
// Note: Didn't need to write 'as const' here
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });

請注意,const 修飾符不拒絕可變值,也不需要不可變約束。使用可變類型約束可能會產生令人驚訝的結果。例如:

1
2
3
4
declare function fnBad<const T extends string[]>(args: T): void;

// 'T' is still 'string[]' since 'readonly ["a", "b", "c"]' is not assignable to 'string[]'
fnBad(["a", "b" ,"c"]);

在這裡,推斷的候選項 Treadonly ["a", "b", "c"],並且 readonly 不能在需要可變數組的地方使用數組。在這種情況下,推理回退到約束條件,數組被視為 string[],並且調用仍然成功進行。

此函數的更好定義應該使用 readonly string[]

1
2
3
4
declare function fnGood<const T extends readonly string[]>(args: T): void;

// T is readonly ["a", "b", "c"]
fnGood(["a", "b" ,"c"]);

同樣,請記住修飾符 const 僅影響在調用中編寫的對象、數組和原始表達式的推斷,因此不會(或不能)修改的參數不會看到 as const 任何行為變化:

1
2
3
4
5
declare function fnGood<const T extends readonly string[]>(args: T): void;
const arr = ["a", "b" ,"c"];

// 'T' is still 'string[]'-- the 'const' modifier has no effect here
fnGood(arr);

有關更多詳細信息,請參閱拉取請求和(第一個第二個)激勵問題。


extends 支持多個配置文件

tsconfig.json 管理多個項目時,擁有一個其他文件可以擴展的"基本"配置文件會很有幫助。這就是為什麼 TypeScript 支持 extendscompilerOptions

1
2
3
4
5
6
7
8
// packages/front-end/src/tsconfig.json
{
  "extends": "../../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "../lib",
    // ...
  }
}

但是,在某些情況下,您可能希望從多個配置文件進行擴展。例如:假設使用發送到 npm 的 TypeScript 基本配置文件。如果您希望所有項目也使用 npm 包中的選項 @tsconfig/strictest,那麼有一個簡單的解決方案:擴展 tsconfig.base.json@tsconfig/strictest

1
2
3
4
5
6
7
// tsconfig.base.json
{
  "extends": "@tsconfig/strictest/tsconfig.json",
  "compilerOptions": {
    // ...
  }
}

這在一定程度上起作用。如果您有任何項目不想使用 @tsconfig/strictest,他們必須手動禁用這些選項,或者創建一個短度的 tsconfig.base.json 版本,該版本不擴展 @tsconfig/strictest

為了在此處提供更多靈活性,Typescript 5.0 現在允許該 extends 字段採用多個條目。例如,在這個配置文件中:

1
2
3
4
5
6
{
  "extends": ["a", "b", "c"],
  "compilerOptions": {
    // ...
  }
}

寫這個有點像 c 直接擴展,其中 c extends bb extends a。如果任何字段"衝突”,則後一個條目獲勝。

所以在下面的例子中,和 strictNullChecksnoImplicitAny 在最終的 tsconfig.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// tsconfig1.json
{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

// tsconfig2.json
{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

// tsconfig.json
{
  "extends": ["./tsconfig1.json", "./tsconfig2.json"],
  "files": ["./index.ts"]
}

再舉一個例子,我們可以用下面的方式重寫我們原來的例子。

1
2
3
4
5
6
7
8
// packages/front-end/src/tsconfig.json
{
  "extends": ["@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json"],
  "compilerOptions": {
    "outDir": "../lib",
    // ...
  }
}

有關更多詳細信息,請閱讀有關原始拉取請求的更多信息。


所有枚舉都是聯合枚舉

當 TypeScript 最初引入枚舉時,它們只不過是一組具有相同類型的數字常量。

1
2
3
4
enum E {
  Foo = 10,
  Bar = 20,
}

E.FooE.Bar 的唯一特別之處在於它們可以賦值給除 E 類型之外的任何類型。除此之外,他們幾乎只是 numbers

1
2
3
4
function takeValue(e: E) {}

takeValue(E.Foo); // works
takeValue(123); // error!

直到 TypeScript 2.0 引入了枚舉文字類型,枚舉才變得更加特殊。枚舉文字類型為每個枚舉成員提供了自己的類型,並將枚舉本身變成了每個成員類型的聯合。它們還允許我們僅引用枚舉類型的一個子集,並縮小這些類型的範圍。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Color is like a union of Red | Orange | Yellow | Green | Blue | Violet
enum Color {
  Red, Orange, Yellow, Green, Blue, /* Indigo */, Violet
}

// Each enum member has its own type that we can refer to!
type PrimaryColor = Color.Red | Color.Green | Color.Blue;

function isPrimaryColor(c: Color): c is PrimaryColor {
  // Narrowing literal types can catch bugs.
  // TypeScript will error here because
  // we'll end up comparing 'Color.Red' to 'Color.Green'.
  // We meant to use ||, but accidentally wrote &&.
  return c === Color.Red && c === Color.Green && c === Color.Blue;
}

為每個枚舉成員賦予其自己的類型的一個問題是,這些類型在某些部分與成員的實際值相關聯。在某些情況下,無法計算該值——例如,枚舉成員可以通過函數調用進行初始化。

1
2
3
enum E {
  Blah = Math.random()
}

每當 TypeScript 遇到這些問題時,它都會悄悄退出並使用舊的枚舉策略。這意味著放棄聯合和文字類型的所有優勢。

TypeScript 5.0 通過為每個計算成員創建唯一類型,設法將所有枚舉變成聯合枚舉。這意味著現在可以縮小所有枚舉的範圍,並將其成員也作為類型引用。

有關此更改的更多詳細信息,您可以閱讀GitHub 上的詳細信息


--moduleResolution 新增 bundler 支持

TypeScript 4.7 在 --module--moduleResolution 設置中引入了 node16nodenext 選項。這些選項的目的是更好地模擬 Node.js 中 ECMAScript 模塊的精確查找規則;然而這種模式有很多限制,其他工具並沒有真正強制執行。

例如,在 Node.js 的 ECMAScript 模塊中,任何相對導入都需要包含文件副檔名。

1
2
3
4
// entry.mjs
import * as utils from "./utils"; //  wrong - we need to include the file extension.

import * as utils from "./utils.mjs"; //  works

在 Node.js 和瀏覽器中這樣做有一定的原因,它使文件查找更快,並且更適合原始文件服務器。但是對於許多使用捆綁器等工具的開發人員來說,node16/nodenext 設置很麻煩,因為捆綁器沒有這些限制中的大部分。在某些方面,node 解析模式對任何使用捆綁器的人來說都更好。

但在某些方面,原有的 node 解決模式已經落伍了。大多數現代捆綁器在 Node.js 中使用 ECMAScript 模塊和 CommonJS 查找規則的融合。例如:無擴展名的導入就像在 CommonJS 中一樣工作得很好,但是在查看包的 export 條件時,他們會更喜歡 ECMAScript 文件中的 import 條件。

為了模擬打包器的工作方式,TypeScript 現在引入了一種新策略:--moduleResolution bundler

1
2
3
4
5
6
{
  "compilerOptions": {
    "target": "esnext",
    "moduleResolution": "bundler"
  }
}

如果您正在使用像 Vite、esbuild、swc、Webpack、Parcel 和其他實施混合查找策略的現代捆綁器,那麼新選項 bundler 應該非常適合您。

另一方面,如果您正在編寫一個打算在 npm 上發布的庫,則使用該 bundler 選項可以隱藏兼容性問題,這些問題可能會出現在您不使用捆綁器的用戶身上。因此,在這些情況下,使用 node16nodenext 解決方案可能是更好的途徑。

要了解更多信息 --moduleResolution bundler,請查看拉取請求


自定義解析標誌

JavaScript 工具現在可以模擬"混合"解析規則,就像 bundler 我們上面描述的模式一樣。由於工具的支持可能略有不同,TypeScript 5.0 提供了啟用或禁用一些功能的方法,這些功能可能適用於您的配置,也可能不適用於您的配置。

allowImportingTsExtensions

--allowImportingTsExtensions 允許使用特定於 TypeScript 的擴展名(如 .ts.mts.tsx)。

此標誌僅在 --noEmit--emitDeclarationOnly 啟用時才被允許,因為這些導入路徑在運行時無法在 JavaScript 輸出文件中解析。這裡的期望是您的解析器(例如您的捆綁器、運行時或其他一些工具)將使這些 .ts 文件之間的導入工作。

resolvePackageJsonExports

--resolvePackageJsonExports 強制 TypeScript 解析 package.json 的 exports 字段,如果曾經從 node_modules 中的讀取過 json 文件。

當配置項 --modulerresolvenode16nodenextbundler 時,該選項默認為 true

resolvePackageJsonImports

--resolvePackageJsonImports 強制 TypeScript 在從其祖先目錄包含 package.json 的文件執行以 # 開頭的查找時查詢 package.json 文件的導入字段。

當配置項 --modulerresolvenode16nodenextbundler 時,該選項默認為 true

allowArbitraryExtensions

在 TypeScript 5.0 中,當導入路徑不是已知 JavaScript 或 TypeScript 文件擴展名的擴展名結尾時,編譯器將以 {file basename}.d.{extension}.ts。例如,如果您在捆綁項目中使用 CSS 加載器,您可能希望為這些樣式表編寫(或生成)聲明文件:

app.css
1
2
3
.cookie-banner {
  display: none;
}
app.d.css.ts
1
2
3
4
declare const css: {
  cookieBanner: string;
};
export default css;
App.tsx
1
2
3
import styles from "./app.css";

styles.cookieBanner; // string

默認情況下,此導入會引發錯誤,讓您知道 TypeScript 不理解此文件類型,並且您的運行時可能不支持導入它。但是如果您已配置運行時或捆綁程序來處理它,則可以使用新的 --allowArbitraryExtensions 編譯器選項來抑制錯誤。

請注意,從歷史上看,通過添加名為 app.css.d.ts 的聲明文件而不是 app.d.css.ts,通常可以達到類似的效果——然而,這只是通過 Node 對 CommonJS 的 require 解析規則起作用。嚴格來說,前者被解釋為一個名為 app.css.js 的 JavaScript 文件的聲明文件。因為相對文件導入需要在 Node 的 ESM 支持中包含擴展名,所以 TypeScript 會在我們的示例中 --moduleResolution node16 或在 nodenext 下的ESM 文件中出錯。

有關更多信息,請閱讀此功能的提案及其相應的拉取請求

customConditions

--customConditions 接收一個附加條件列表,當 TypeScript 從 package.json 的 exports 或 imports 字段解析時,這些條件將添加到解析器默認使用的任何現有條件中。

例如:當在 tsconfig.json 中設置此字段時:

1
2
3
4
5
6
7
{
  "compilerOptions": {
    "target": "es2022",
    "moduleResolution": "bundler",
    "customConditions": ["my-condition"]
  }
}

任何時候在 package.json 中引用 exportsimports 字段時,TypeScript 都會考慮調用 my-condition 的條件。

因此,當從具有以下內容的包中導入時 package.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  // ...
  "exports": {
    ".": {
      "my-condition": "./foo.mjs",
      "node": "./bar.mjs",
      "import": "./baz.mjs",
      "require": "./biz.mjs"
    }
  }
}

TypeScript 將嘗試查找與 foo.mjs 對應的文件。

該字段僅在 --moduleResolutionnode16nodenextbundler 選項下有效。


--verbatimModuleSyntax

默認情況下,TypeScript 會做一些叫做 import elision 的事情。基本上,如果你寫類似:

1
2
3
4
5
import { Car } from "./car";

export function drive(car: Car) {
  // ...
}

TypeScript 檢測到您只對類型使用導入,所以輸出結果會將此導入代碼刪除。您的輸出 JavaScript 可能看起來像這樣:

1
2
3
export function drive(car) {
  // ...
}

大多數時候這很好,因為如果 Car 不是從 ./car 導出的值,我們將收到運行時錯誤。

但它確實為某些邊緣情況增加了一層複雜性。例如:請注意沒有像這樣的語句 import "./car";,導入被完全刪除。這實際上對有無副作用的模塊產生影響。

TypeScript 針對 JavaScript 的 emit 策略還有另外幾層複雜性,導入省略並不總是由導入的使用方式驅動,它通常還會參考值的聲明方式。所以並不總是很清楚是否像下面這樣的代碼:

1
export { Car } from "./car";

應該保留或丟棄。如果 Carclass 之類的東西聲明,那麼它可以保存在生成的 JavaScript 文件中。但如果 Car 僅聲明為 type 別名或 interface,則 JavaScript 文件 Car 根本不應導出。

雖然 TypeScript 可能能夠根據來自跨文件的信息做出這些發出決定,但並非每個編譯器都可以。

imports 和 exports 的修飾符 type 對這些情況有點幫助。我們可以明確指出導入或導出是否僅用於類型分析,並且可以通過使用修飾符將其完全刪除到 JavaScript 文件中 type

1
2
3
4
5
6
// This statement can be dropped entirely in JS output
import type * as car from "./car";

// The named import/export 'Car' can be dropped in JS output
import { type Car } from "./car";
export { type Car } from "./car";

type 修飾符本身並不是很有用,默認情況下,模塊省略仍然會刪除導入,並且沒有什麼強制您區分 type 普通導入和導出。所以 TypeScript 有標誌 --importsNotUsedAsValues 來確保你使用 type 修飾符,--preserveValueImports 以防止某些模塊省略行為,並 --isolatedModules 確保你的 TypeScript 代碼適用於不同的編譯器。不幸的是,很難理解這 3 個標誌的細節,並且仍然存在一些具有意外行為的邊緣情況。

--verbatimModuleSyntax TypeScript 5.0 引入了一個名為簡化情況的新選項。規則要簡單得多:任何沒有 type 修飾符的導入或導出都會被保留。任何使用 type 修飾符的東西都會被完全丟棄。

1
2
3
4
5
6
7
8
// Erased away entirely.
import type { A } from "a";

// Rewritten to 'import { b } from "bcd";'
import { b, type c, type d } from "bcd";

// Rewritten to 'import {} from "xyz";'
import { type xyz } from "xyz";

有了這個新選項,所見即所得。

不過當涉及到模塊互操作時,這確實有一些影響。在此標誌下,當您的設置或文件擴展名暗示不同的模塊系統時,ECMAScript importsexports 不會被重寫為 require 調用。相反,你會得到一個錯誤。如果您需要發出使用 requiremodule.exports 的代碼,則必須使用早於 ES2015 的 TypeScript 模塊語法:

輸入 TypeScript輸出 JavaScript
import foo = require(“foo”);const foo = require(“foo”);
function foo() {}
function bar() {}
function baz() {}
export = { foo, bar, baz };
function foo() {}
function bar() {}
function baz() {}
module.exports = { foo, bar, baz };

雖然這是一個限制,但它確實有助於使一些問題更加明顯。例如,忘記在 package.json 中設置 type 字段是很常見的。--module node16。因此,開發人員會在沒有意識到的情況下開始編寫 CommonJS 模塊而不是 ES 模塊,從而提供令人驚訝的查找規則和 JavaScript 輸出。這個新標誌確保您有意使用您正在使用的文件類型,因為語法是有意不同的。

因為 --verbatimModuleSyntax 提供了比 --importsNotUsedAsValues--preserveValueImports 更一致的故事,所以這兩個現有的標誌已被棄用。

有關更多詳細信息,請閱讀原始拉取請求及其提案問題


支持 export type *

當 TypeScript 3.8 引入純類型導入時,新語法不允許用於 export * from "module"export * as ns from "module" 重新導出。TypeScript 5.0 添加了對這兩種形式的支持:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// models/vehicles.ts
export class Spaceship {
 // ...
}

// models/index.ts
export type * as vehicles from "./vehicles";

// main.ts
import { vehicles } from "./models";

function takeASpaceship(s: vehicles.Spaceship) {
 // ok - `vehicles` only used in a type position
}

function makeASpaceship() {
 return new vehicles.Spaceship();
 //         ^^^^^^^^
 // 'vehicles' cannot be used as a value because it was exported using 'export type'.
}

您可以在此處閱讀有關實施的更多信息。


JSDoc 支持 @satisfies

TypeScript 4.9 引入了 satisfies 運算符。它確保表達式的類型兼容,而不影響類型本身。例如,讓我們看下面的代碼:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface CompilerOptions {
  strict?: boolean;
  outDir?: string;
  // ...
}

interface ConfigSettings {
  compilerOptions?: CompilerOptions;
  extends?: string | string[];
  // ...
}

let myConfigSettings = {
  compilerOptions: {
    strict: true,
    outDir: "../lib",
    // ...
  },

  extends: [
    "@tsconfig/strictest/tsconfig.json",
    "../../../tsconfig.base.json"
  ],

} satisfies ConfigSettings;

在這裡,TypeScript 知道它 myConfigSettings.extends 是用數組聲明的,因為在 satisfies 驗證我們對象的類型時,它並沒有直接將其更改為 ConfigSettings 並丟失信息。所以如果我們想映射過來 extends,那很好。

1
2
3
declare function resolveConfig(configPath: string): CompilerOptions;

let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);

這對 TypeScript 用戶很有幫助,但是很多人使用 TypeScript 來使用 JSDoc 註釋對他們的 JavaScript 代碼進行類型檢查。這就是為什麼 TypeScript 5.0 支持一個名為 JSDoc 的新標籤,@satisfies 它做的事情完全一樣。

/** @satisfies */ 可以捕獲類型不匹配:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// @ts-check

/**
 * @typedef CompilerOptions
 * @prop {boolean} [strict]
 * @prop {string} [outDir]
 */

/**
 * @satisfies {CompilerOptions}
 */
let myCompilerOptions = {
  outdir: "../lib",
  // ~~~~~~ oops! we meant outDir
};

但它會保留我們表達式的原始類型,允許我們稍後在代碼中更精確地使用我們的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// @ts-check

/**
 * @typedef CompilerOptions
 * @prop {boolean} [strict]
 * @prop {string} [outDir]
 */

/**
 * @typedef ConfigSettings
 * @prop {CompilerOptions} [compilerOptions]
 * @prop {string | string[]} [extends]
 */


/**
 * @satisfies {ConfigSettings}
 */
let myConfigSettings = {
  compilerOptions: {
    strict: true,
    outDir: "../lib",
  },
  extends: [
    "@tsconfig/strictest/tsconfig.json",
    "../../../tsconfig.base.json"
  ],
};

let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);

/** @satisfies */ 也可以在任何帶括號的表達式上內聯使用。我們可以 myConfigSettings 這樣寫:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let myConfigSettings = /** @satisfies {ConfigSettings} */ ({
  compilerOptions: {
    strict: true,
    outDir: "../lib",
  },
  extends: [
    "@tsconfig/strictest/tsconfig.json",
    "../../../tsconfig.base.json"
  ],
});

為什麼?當您更深入地了解其他一些代碼(例如函數調用)時,它通常更有意義。

1
2
3
compileCode(/** @satisfies {ConfigSettings} */ ({
  // ...
}));

此功能由 Oleksandr Tarasiuk提供!


JSDoc 支持 @overload

在 TypeScript 中,您可以為函數指定重載。重載為我們提供了一種方式,可以用不同的參數調用一個函數,並可能返回不同的結果。他們可以限制調用者實際使用我們函數的方式,並優化他們將返回的結果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Our overloads:
function printValue(str: string): void;
function printValue(num: number, maxFractionDigits?: number): void;

// Our implementation:
function printValue(value: string | number, maximumFractionDigits?: number) {
  if (typeof value === "number") {
    const formatter = Intl.NumberFormat("en-US", {
      maximumFractionDigits,
    });
    value = formatter.format(value);
  }

  console.log(value);
}

在這裡,我們說過 printValue 將 string 或 number 作為其第一個參數。如果它需要一個 number,它可以使用第二個參數來確定我們可以打印多少個小數位。

TypeScript 5.0 現在允許 JSDoc 使用新標籤聲明重載 @overload。每個帶有標記的 JSDoc 註釋都 @overload 被視為以下函數聲明的不同重載。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// @ts-check

/**
 * @overload
 * @param {string} value
 * @return {void}
 */

/**
 * @overload
 * @param {number} value
 * @param {number} [maximumFractionDigits]
 * @return {void}
 */

/**
 * @param {string | number} value
 * @param {number} [maximumFractionDigits]
 */
function printValue(value, maximumFractionDigits) {
  if (typeof value === "number") {
    const formatter = Intl.NumberFormat("en-US", {
      maximumFractionDigits,
    });
    value = formatter.format(value);
  }

  console.log(value);
}

現在,無論我們是在 TypeScript 還是 JavaScript 文件中編寫,TypeScript 都可以讓我們知道我們是否錯誤地調用了我們的函數。

1
2
3
4
5
6
// all allowed
printValue("hello!");
printValue(123.45);
printValue(123.45, 2);

printValue("hello!", 123); // error!

這個新標籤的實現要歸功於 Tomasz Lenarcik


在tsc --build 時可以傳入的新指令

TypeScript 現在允許在 --build 模式下傳遞以下指令:

  • --declaration
  • --emitDeclarationOnly
  • --declarationMap
  • --sourceMap
  • --inlineSourceMap

這使得自定義構建的某些部分變得更加容易,您可能有不同的開發和生產構建。

例如,庫的開發構建可能不需要生成聲明文件,但生產構建需要。項目可以將聲明發射配置為默認關閉,只需使用:

1
tsc --build -p ./my-project-dir

一旦在內循環中完成迭代,“生產"構建就可以傳遞指令 --declaration

1
tsc --build -p ./my-project-dir --declaration

有關此更改的更多信息,請參見此處


編輯器中不區分大小寫的導入排序

在 Visual Studio 和 VS Code 等編輯器中,TypeScript 支持組織和排序導入和導出的體驗。但是,對於列表何時"排序”,通常會有不同的解釋。

例如,下面的導入列表是否排序?

1
2
3
4
5
import {
  Toggle,
  freeze,
  toBoolean,
} from "./utils";

答案可能令人驚訝地是"視情況而定"。如果我們不關心區分大小寫,那麼這個列表顯然沒有排序。這封信 f 出現在 t 和之前 T

但在大多數編程語言中,排序默認是比較字符串的字節值。JavaScript 比較字符串的方式意味著 "Toggle" 總是在前面 "freeze",因為根據 ASCII 字符編碼,大寫字母在小寫字母之前。所以從這個角度來看,導入列表是排序的。

TypeScript 之前考慮對導入列表進行排序,因為它正在進行基本的區分大小寫的排序。對於喜歡不區分大小寫排序的開發人員,或者使用像 ESLint 這樣默認需要不區分大小寫排序的工具的開發人員來說,這可能是一個令人沮喪的地方。

TypeScript 現在默認檢測區分大小寫。這意味著 TypeScript 和 ESLint 等工具通常不會就如何最好地對導入進行排序而相互"爭吵"。

我們的團隊也一直在試驗進一步的排序策略,您可以在此處閱讀有關內容。這些選項最終可能由編輯器配置。目前,它們仍然不穩定且處於試驗階段,您現在可以通過使用 typescript.unstableJSON 選項中的條目在VS Code 中選擇加入它們。以下是您可以嘗試的所有選項(設置為默認值):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
  "typescript.unstable": {
  // Should sorting be case-sensitive? Can be:
  // - true
  // - false
  // - "auto" (auto-detect)
  "organizeImportsIgnoreCase": "auto",

  // Should sorting be "ordinal" and use code points or consider Unicode rules? Can be:
  // - "ordinal"
  // - "unicode"
  "organizeImportsCollation": "ordinal",

  // Under `"organizeImportsCollation": "unicode"`,
  // what is the current locale? Can be:
  // - [any other locale code]
  // - "auto" (use the editor's locale)
  "organizeImportsLocale": "en",

  // Under `"organizeImportsCollation": "unicode"`,
  // should upper-case letters or lower-case letters come first? Can be:
  // - false (locale-specific)
  // - "upper"
  // - "lower"
  "organizeImportsCaseFirst": false,

  // Under `"organizeImportsCollation": "unicode"`,
  // do runs of numbers get compared numerically (i.e. "a1" < "a2" < "a100")? Can be:
  // - true
  // - false
  "organizeImportsNumericCollation": true,

  // Under `"organizeImportsCollation": "unicode"`,
  // do letters with accent marks/diacritics get sorted distinctly
  // from their "base" letter (i.e. is é different from e)? Can be
  // - true
  // - false
  "organizeImportsAccentCollation": true
  },
  "javascript.unstable": {
    // same options valid here...
  },
}

您可以閱讀有關自動檢測和指定不區分大小寫的原始工作的更多詳細信息,然後是更廣泛的選項集


switch/case 語法補足

在編寫 switch 語句時,TypeScript 現在會檢測被檢查的值何時具有文字類型。如果是這樣,它將提供一個完成每個未發現的腳手架 case

您可以在 GitHub 上查看實施細節。


速度、內存和包大小優化

TypeScript 5.0 在我們的代碼結構、數據結構和算法實現中包含許多強大的變化。這些都意味著你的整個體驗應該更快,不僅僅是運行 TypeScript,甚至安裝它。

以下是我們相對於 TypeScript 4.9 在速度和大小方面取得的一些有趣的勝利。

設想時間或大小相對於 TS 4.9
material-ui 構建時間90%
TypeScript 編譯器啟動時間89%
編劇建造時間88%
TypeScript Compiler 自建時間87%
Outlook Web 構建時間82%
VS 代碼構建時間80%
打字稿 npm 包大小59%

如何?有一些顯著的改進,我們希望在未來提供更多細節。但我們不會讓您等待那篇博文。

首先,我們最近將 TypeScript 從命名空間遷移到模塊,使我們能夠利用現代構建工具來執行範圍提升等優化。使用此工具、重新審視我們的打包策略並刪除一些已棄用的代碼,已將 TypeScript 4.9 的 63.8 MB 包大小減少了約 26.4 MB。它還通過直接函數調用為我們帶來了顯著的加速。我們在這裡整理了一份關於我們遷移到模塊的詳細文章

TypeScript 還為編譯器中的內部對像類型增加了更多的統一性,並且還精簡了存儲在其中一些對像類型上的數據。這減少了多態操作,同時平衡了因使我們的對象形狀更統一而增加的內存使用量。

在將信息序列化為字符串時,我們還執行了一些緩存。類型顯示可能作為錯誤報告、聲明發出、代碼完成等的一部分發生,最終可能會相當昂貴。TypeScript 現在緩存了一些常用的機制以在這些操作中重用。

我們做出的另一個改進解析器的顯著變化是利用 var 偶爾迴避使用 letconst 跨閉包的成本。這提高了我們的一些解析性能。

總的來說,我們預計大多數代碼庫應該會看到 TypeScript 5.0 的速度提升,並且始終能夠重現 10% 到 20% 之間的勝利。當然,這將取決於硬件和代碼庫特性,但我們鼓勵您今天就在您的代碼庫上嘗試一下!

有關詳細信息,請參閱我們的一些顯著優化:


重大更改和棄用

運行時要求

TypeScript 現在以 ECMAScript 2018 為目標。TypeScript 包還設置了最低預期引擎 12.20。對於 Node 用戶,這意味著 TypeScript 5.0 的最低版本要求至少為 Node.js 12.20 及更高版本。

lib.d.ts變化

更改 DOM 類型的生成方式可能會對現有代碼產生影響。值得注意的是,某些屬性已從 number 數字文字類型轉換為數字文字類型,並且用於剪切、複製和粘貼事件處理的屬性和方法已跨界面移動。

API 重大變更

在 TypeScript 5.0 中,我們轉向了模塊,刪除了一些不必要的接口,並進行了一些正確性改進。有關更改內容的更多詳細信息,請參閱我們的 API 重大更改頁面。

關係運算符中禁止的隱式強制轉換

如果您編寫的代碼可能會導致隱式的字符串到數字強制轉換,則 TypeScript 中的某些操作會警告您:

1
2
3
function func(ns: number | string) {
  return ns * 4; // Error, possible implicit coercion
}

在 5.0 中,這也將應用於關係運算符 ><<=>=

1
2
3
function func(ns: number | string) {
  return ns > 4; // Now also an error
}

如果需要,要允許這樣做,您可以顯式地將操作數強制為 number using +

1
2
3
function func(ns: number | string) {
  return +ns > 4; // OK
}

正確性改進Mateusz Burzyński 提供。

枚舉大修

enum 自從它的第一個版本以來,TypeScript 就一直存在一些關於 s 的奇怪之處。在 5.0 中,我們正在清理其中的一些問題,並減少理解 enum 您可以聲明的各種 s 所需的概念數。

作為其中的一部分,您可能會看到兩個主要的新錯誤。首先是將域外文字分配給類型 enum 現在會像人們預期的那樣出錯:

1
2
3
4
5
6
7
8
enum SomeEvenDigit {
  Zero = 0,
  Two = 2,
  Four = 4
}

// Now correctly an error
let m: SomeEvenDigit = 1;

另一個是用混合數字和間接字符串枚舉引用聲明值的枚舉會錯誤地創建一個全數字 enum

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
enum Letters {
  A = "a"
}
enum Numbers {
  one = 1,
  two = Letters.A
}

// Now correctly an error
const t: number = Numbers.two;

您可以在相關更改中查看更多詳細信息。

對構造函數中的參數裝飾器進行更準確的類型檢查 --experimentalDecorators

TypeScript 5.0 使 --experimentalDecorators。這一點變得明顯的一個地方是在構造函數參數上使用裝飾器時。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
export declare const inject:
  (entity: any) =>
    (target: object, key: string | symbol, index?: number) => void;

export class Foo {}

export class C {
  constructor(@inject(Foo) private x: any) {
  }
}

此調用將失敗,因為 key 需要一個 string | symbol,但構造函數參數收到一個鍵 undefined。key 正確的解決方法是更改 within 的類型 inject。如果您使用的是無法升級的庫,一個合理的解決方法是包裝 inject 一個類型更安全的裝飾器函數,並在 key

更多詳情,請參閱本期

棄用和默認更改

在 TypeScript 5.0 中,我們棄用了以下設置和設置值:

  • --target: ES3
  • --out
  • --noImplicitUseStrict
  • --keyofStringsOnly
  • --suppressExcessPropertyErrors
  • --suppressImplicitAnyIndexErrors
  • --noStrictGenericChecks
  • --charset
  • --importsNotUsedAsValues
  • --preserveValueImports
  • prepend 在項目參考中

在 TypeScript 5.5 之前,這些配置將繼續被允許,屆時它們將被完全刪除,但是,如果您正在使用這些設置,您將收到警告。在 TypeScript 5.0 以及未來版本 5.1、5.2、5.3 和 5.4 中,您可以指定 "ignoreDeprecations": "5.0" 屏蔽這些警告提示。我們還將很快發布一個4.9 補丁,以允許指定 ignoreDeprecations 以允許更平滑的升級。除了棄用之外,我們還更改了一些設置以更好地改進 TypeScript 中的跨平台行為。

--newLine,它控制 JavaScript 文件中發出的行尾,如果未指定,過去常常根據當前操作系統進行推斷。我們認為構建應該盡可能具有確定性,並且 Windows 記事本現在支持換行符行結尾,因此新的默認設置是 LF。舊的特定於操作系統的推理行為不再可用。

--forceConsistentCasingInFileNames,這確保了項目中對同一文件名的所有引用都在大小寫中達成一致,現在默認為 true。這有助於捕獲在不區分大小寫的文件系統上編寫的代碼的差異問題。

您可以留下反饋並查看有關 5.0 棄用跟踪問題的更多信息


下一步是什麼?

不要操之過急,TypeScript 5.1 已經在開發中了,我們所有的計劃都已經在 GitHub 上了。如果你躍躍欲試,我們鼓勵你嘗試 TypeScript 的每日構建版本或針對 VS Code 的 JavaScript 和 TypeScript Nightly 擴展

當然,如果您選擇只享受 TypeScript 的新穩定版,我們也不會感到受傷。我們希望 TypeScript 5.0 讓每個人的編碼更快、更有趣。

Happy Hacking!

by Daniel Rosenwasser 和 TypeScript 團隊


comments powered by Disqus