NHN Cloud NHN Cloud Meetup!

TypeScript 4.0 betaのリリース内容

はじめに

TypeScript 4.0 betaバージョンがリリースされました。どのような機能が新たにサポートされるのか、内容を調べてみましょう。

インストール

npm install typescript@beta

可変引数(variadic)タプル型

variadic:プログラミング言語で、関数の引数が固定ではなく任意の個数となっている引数(argument)のこと

  • 1)ジェネリック型をスプレッド(spread)演算子と使用できる

    function tail<T extends any[]>(arr: readonly [any, ...T]) {
        const [_ignored, ...rest] = arr;
        return rest;
    }
    
    const myTuple = [1, 2, 3, 4] as const;
    const myArray = ["hello", "world"];
    
    // r1: [2, 3, 4]
    const r1 = tail(myTuple);
    
    // r2: [2, 3, ...string[]]
    const r2 = tail([...myTuple, ...myArray] as const);
  • 2)タプル型をスプレッド演算子で型定義できる
    4.0より前のバージョンでは、タプル型で定義されたタイプをスプレッド演算子を使って新しいタイプに指定しようとすると、エラーになりました。
    配列型のみスプレッド演算子を使って新しいタイプを作ることができましたが、 4.0ではすでに宣言されたタプル型にスプレッド演算子を用いて新しいタイプを作成することができます。
/* 4.0未満 */
type Strings = [string, string];
type Numbers = [number, number];

type NumNum = [...Numbers];
//             ~~~~~~~~~~
// Error! A rest element type must be an array type.

type StrStrNumNum = [...Strings, ...Numbers];
//                   ~~~~~~~~~~
// Error! A rest element must be last in a tuple type.

type NumArr = number[];
type Nums = [...NumArr]; // OK
/* 4.0 */
type Strings = [string, string];
type Numbers = [number, number];

// [string, string, number, number]
type StrStrNumNum = [...Strings, ...Numbers];
  • 配列のどの位置でもスプレッド演算子を使って拡張可能ですが、長さが定まっていない配列型が拡張されると、残りの要素型を含めて連続した配列型を持つようになります。
type Strings = [string, string];
type Numbers = number[]

// [string, string, ...(number | boolean)[]]
type Unbounded = [...Strings, ...Numbers, boolean];
  • 1)と2)を活用して、concat関数のシグネチャ型を定義できます。
type Arr = readonly any[];

function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
    return [...arr1, ...arr2];
}

type NumNumNum = [number, number, number];
type StrStrStr = [string, string, string];

const numArr: NumNumNum = [1, 2, 3];
const strArr: StrStrStr = ['NHN', 'FE', 'TOAST UI'];

// [number, number, number, string, string, string]
const test = concat(numArr, strArr);

test[0] = 'I\'m string';
// 4.0では、タプル型定義で0インデックスがstring型でなくnumber型でなければならないため、エラー発生
  • ここで別の例を挙げましょう。関数のパラメーターを部分的に適用して新しい関数を返すpartialCall関数があると仮定します。
function partialCall(f, ...headArgs) {
    return (...tailArgs) => f(...headArgs, ...tailArgs)
}

// 4.0でタプル型とスプレッド演算子を使って型定義
type Arr = readonly unknown[];

function partialCall<T extends Arr, U extends Arr, R>(f: (...args: [...T, ...U]) => R, ...headArgs: T) {
    return (...b: U) => f(...headArgs, ...b)
}

4.0では、タプル型のスプレッド演算子を活用すると、partialCallのような複雑な関数のパラメータの型推論をより正確に行えます。

const foo = (x: string, y: number, z: boolean) => {}

// xの型のnumberではなく、stringであるためエラー発生
const f1 = partialCall(foo, 100);
//                          ~~~

// 関数fooが受信できるパラメータの個数が異なるためエラー発生
const f2 = partialCall(foo, "hello", 100, true, "oops")
//                                              ~~~~~~

const f3 = partialCall(foo, "hello"); // 정상 동작 'f3: (y: number, z: boolean) => void'

f3(123, true); // 正常動作

f3(); // 渡すべきパラメータが2つあるためエラー発生

// 2番目のパラメータはbooleanであるが、string型を渡すためエラー発生
f3(123, "hello");
//      ~~~~~~~
  • concattailpartialCallのように、可変引数を受け取る関数でタプル型を用いると多くのパターンで活用できます。
  • JavaScriptに内蔵された(built-inbindメソッドで使用するとき、型チェックがより上手に行えます。

名前を付けられるタプル要素

  • 関数のパラメーターをタプル型で宣言して使用すると、型チェックには影響を及ぼしませんが、可読性の面ではパラメーターの意図を把握するのが難しくなります。
function foo1(...args: [string, number]): void {
  // ...
}

function foo2(arg0: string, arg1: number): void {
  // ...
}
  • タプルに名前を付けられます。
type Range = [start: number, end: number];
type Foo = [first: number, second?: string, ...rest: any[]];

type Bar = [first: string, number];
//                         ~~~~~~
// Error! Tuple members must all have names or all not have names.
// 使用例
function foo3(x: [first: string, second: number]) {
  // ...
  let [a, b] = x;
}
  • タプルとパラメーターリストを使用しているパターンにおいて、type-safe方式で使用できます。

type-safe:データ型の食い違いによって起こるエラーを防ぐ仕組み

クラスコンストラクタから属性型を推論

  • 4.0では、noImplicitAnyオプションを有効にすると、クラスの属性型を決定する制御フロー分析(CFA:control flow analysis)を使用することができます。

CFA:プログラムの制御フローを決定するための静的コード解析手法

4.0以前)
4.0)

  • strictPropertyInitialization: trueオプションを有効にすると、undefined型になるときにエラーを表示します。
class Square {
    sideLength;

    constructor(sideLength: number) {
        if (Math.random()) {
            this.sideLength = sideLength;
        }
    }

    get area() {
        return this.sideLength ** 2;
        //     ~~~~~~~~~~~~~~~
        // Error! Object is possibly 'undefined'.
    }
}
  • コンストラクタから別のメソッドを呼び出し、値を割り当てると属性型はanyになります。
// AS-IS
class Square {
  sideLength; // any型でエラー発生

  constructor(sideLength: number) {
    this.initialize(sideLength);
    // initializeメソッドで当該メソッドを割り当てている場合、sideLengthはany型でエラー発生
  }

  initialize(sideLength: number) {
    this.sideLength = sideLength;
  }

  get area() {
    return this.sideLength ** 2;
  }
}

このような状況では、!Definite Assignment Assertions)を使った既存の方法で処理します。

// TO-BE
class Square {
  /*
  インスタンス属性変数を宣言するとき、変数名の後ろに「!」をつけて
  TypeScriptコンパイラに当該変数が実際に割り当てられたと見做して、エラーを表示しないようにする。
  */
  sideLength!: number; // 明示的型宣言

  constructor(sideLength: number) {
    this.initialize(sideLength);
  }

  initialize(sideLength: number) {
    this.sideLength = sideLength;
  }

  get area() {
    return this.sideLength ** 2;
  }
}

 

段落(Short-Circuiting)代入演算子

  • 複合(compound)代入演算子は、演算子を2つの引数に適用して、その結果を左側に割り当てます。
// 複合代入演算子の例
a += b;
a -= b;
a *= b;
a /= b;
a **= b;
a <<= b;
  • TypeScript 4.0は、3つの新しい複合代入演算子&&=||=??=を追加でサポートします。
    1)and論理演算子:&&
    2)or論理演算子:||
    3)null連結演算子(nullish coalescing operator):??
a &&= b;        // a = a && b;
a ||= b;        // a = a || b;
a ??= b;        // a = a ?? b;

// a ||= b; 実際に評価されるのは
a || (a = b);

// < 4.0
(values ?? (values = [])).push("hello");

// 4.0
(values ??= []).push("hello");

TC39 論理代入演算子の提案(proposal)関連リンク

catch節のパラメータータイプを指定

try {
    // ...
} catch (x) { // 4.0以前はxの型は指定不可
    // xの型は'any'
    console.log(x.message);
    console.log(x.toUpperCase());
    x++;
    x.yadda.yadda.yadda();
}
  • 4.0では、catch節のパラメータータイプをunknown、またはanyに指定可能です。
  • ErrorExceptionのように例外処理クラスを直接定義してタイプに指定することはできません。
  • typeofまたはinstanceofを使って、タイプを絞り込んで使用できます。
try {
  // ...
} catch (e: unknown) {
    console.log(e.toUpperCase());
    //          ~
    // Error! Object is of type 'unknown'.

    if (typeof e === "string") {
      console.log(e.toUpperCase());
    }

    if (e instanceof ErrorExcetion) {
      console.log(e.errorMessage);
    }
}

 

JSXファクトリーのカスタマイズ

  • JSX使用時、フラグメント(Fragment)は複数の子要素を返却できるようにします。
  • 4.0では新しいjsxFragemntFactoryオプションを使って、フラグメントファクトリーをユーザー定義して使用することができます。
  • tsconfig.jsonファイル設定でTypeScriptがJSXを変換する際にReactと互換性のある方法を設定できます。
// tsconfig.json例
{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "jsx": "react",
    "jsxFactory": "h",               // React.createElementの代わりにhを使用
    "jsxFragmentFactory": "Fragment" //  React.Fragmentの代わりにFragmentを使用
  }
}
  • ファイル別に異なるJSXファクトリを使用する場合は、新しいプラグマコメント/** @jsxFrag */)を使用できます。
// JSDocスタイルの行構文を使って適用

/** @jsx h */
/** @jsxFrag Fragment */
import { h , Fragment } from 'preact';

let stuff = <>
    <div>Hello</div>
    <div>Jodeng</div>
</>;

JavaScript変換結果

import { h, Fragment } from "preact";
let stuff = h(Fragment, null,
    h("div", null, "Hello"));

–noEmitOnErrorオプションを使うビルドモードで速度向上

  • 4.0以前は、インクリメンタルビルドのとき–noEmitOnErrorオプションを有効にするとビルド速度が非常に遅くなりました。
    • その理由は、–noEmitOnErrorオプションを基準に、.tsbuildinfoファイルで最後にコンパイルされた結果がキャッシュされていないためです。
  • 4.0では、–buildモードのシナリオ(–incremental–noEmitOnError)を向上させ、速度が大幅に改善されました。

–incremental–noEmitオプションを一緒に使用可能

  • –incrementalオプションを使用しつつ、–noEmitフラグも一緒に使用できるようになりました。
    • 4.0以前は、–incremetalは.tsbuildinfoファイルを生成する必要があるため、許可されていませんでした。

エディターの改善(Visual Studio Code)

/** @deprecated */JSDocコメントに対応

  • 宣言時、/** @deprecated */のようにJSDocコメントを作成すると、取り消し線のスタイルで表示されます。

起動時に部分編集モードをサポート

  • 大規模プロジェクトで起動時間が遅い問題がありました。
    • 原因は、プロジェクトローディングと呼ばれるプロセスで、コンパイラのプログラム構成段階とほぼ同じ作業を行うためです。
    • ファイルの初期設定から始まり、ファイルの構文解析、依存関係の解決、依存関係の解析などの作業に時間がかかります。
    • プロジェクトが大きいほど、コード補完機能[go-to-definition]のようなエディターのデフォルト動作を実行するため、起動時間の遅延がより激しくなります。
  • 全ての言語サービス環境がロードされるまでの間にも、エディターで編集が可能になるよう、新しいモードを開発してきました。
    • 単一ファイルを見るだけという、軽微な仕事をするサーバーの一部機能をエディターによって起動できないだろうかというのが、発想の元になっています。

TypeScriptがVisual Studio Codeのコードベースのファイルから応答するまでに、20秒から1分程度の時間がかかっていますが、新しくサポートされるモードは、コードベースからTypeScriptが対話モードになるまでの時間を2-5秒間に減らすことができるでしょう。

  • これらのモードは、Visual Studio Code Insidersで事前に試すことができます。
    1)Visual Studio Code Insidersインストール
    2)JavaScript and TypeScript Nightlyエクステンションをインストール
    3)エディタの設定ファイル(settings.json)に、以下を内容を追加
"typescript.tsserver.useSeparateSyntaxServer": "dynamic",

 

よりスマートになった自動インポート機能

Q. @typesパッケージでは正常動作しますが、独自にtypesを持つプロジェクトパッケージはなぜ自動で取得できないのでしょうか?
A. TypeScriptでは、node_modules@typeesにあるすべてのパッケージは自動的に含まれますが、他のパッケージは含まれません。
すべてのノード_modulesにあるパッケージをクローリングすると、コストが高くなる場合があるからです。

パッケージをインストールし、まだ使用していないものを自動的にインポートしようとすると、起動がうまくいかないことがあるかもしれません。

TypeScript 4.0は、エディタで自動インポート機能を使用するため、package.jsonのdependenciesフィールドに表示されたパッケージリストを含んでおり、若干の追加作業が実行されます。このパッケージ情報を取得するのは、単に自動インポート機能を向上させるためで、node_modulesディレクトリをすべてクローリングするコストを削減できます。

Breaking Changes

lib.d.tsファイルの変更

  • DOM関連のタイプが変更されました。
    • IEとSafariでのみ動作していたdocument.originを除去(MDNではself.originの使用を推奨)

親クラスの属性を再定義(override)すると無条件でエラーを表示

  • 4.0以前では、useDefineForClassFieldオプションを有効にした状態で、子クラスにおいて親クラスの属性を再定義すると、TypeScriptのコンパイルエラーが発生しました。
  • 4.0ではオプションの使用有無に関係なく、親クラスのgetterまたはsetterを派生した子クラスで再定義すると、常にエラーを表示します。
class Base {
  name = 'FE Dev Lab';

  get foo() {
      return 100;
  }
  set foo(value: number) {
      // ...
  }
}

class Derived extends Base {
  foo = 10;
//~~~
// Error! 
// 'foo' is defined as an accessor in class 'Base',
// but is overridden here in 'Derived' as an instance property.

  get name() {
    //~~~~
    // Error! 
    // 'name' is defined as an accessor in class 'Base',
    // but is overridden here in 'Derived' as an accessor.
    return 'TOAST UI Team';
  }
}

 

delete演算子のオペランド型はオプショナル属性でなければならない

  • strictNullChecks: trueオプションが有効であるとき、delete使用時のオペランド型はanyunknownneverとオプショナル属性で指定する必要があります。
  • 必須属性をdeleteしようとすると、コンパイルエラーを表示します。
interface Thing {
  prop: string;
  optionalProp?: number
  anyProp: any;
  unknownProp: unknown;
  neverProp: never;
}

function f(x: Thing) {
  delete x.prop;
  //     ~~~~~~
  // Error! The operand of a 'delete' operator must be optional.

  delete x.optionalProp;
  delete x.anyProp;
  delete x.unknownProp;
  delete x.neverProp;
}

 

TypeScriptのNode Factoryは使用できない

  • TypeScriptは、ノード作成用の抽象構文ツリー(AST- Abstract Syntax Tree)「ファクトリ」関数集合を提供していますが、4.0では新しいファクトリAPIを提供します。
  • TypeScript 4.0では新しいAPIを使用し、すでに作成された関数は、これ以上使用できないように(deprecated)なりました。

抽象構文ツリー(Abstract Syntax Tree):プログラミング言語の文法によってソースコードの構造を表示する階層的プログラム表現(respresentation

参考

https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-beta
https://en.wikipedia.org/wiki/Variadic_function#In_JavaScript
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#definite-assignment-assertions
https://github.com/tc39/proposal-logical-assignment/

NHN Cloud Meetup 編集部

NHN Cloudの技術ナレッジやお得なイベント情報を発信していきます
pagetop