🔖

TypeScript 4.2 覚書

2021/02/24に公開
4

https://devblogs.microsoft.com/typescript/announcing-typescript-4-2-beta/
こちらの記事のまとめになります。

タプルの先頭と途中でもRest Elementsを置けるようになる

タプルの先頭と途中にRestElementsを置くことができるようになります。
タプルいいですよね。僕は好きです。

// leading Rest Element
let a = [...string[], boolean];
a = [true];
a = ['1', '2', false];

// middle Rest Element
let b = [boolean, ...string[], number];
b = [true, 10];
b = [true, 'a', 'b', 2];

ただし、restは一回だけしかつかえないのと、optionalを併用できないところです。

let foo: [...string[], number, ...boolean[]]; // Error
let hoge: [...string[], number?]; // Error

これはエラーになります。これらの型を定義してしまうとタプルのもつ法則性がなくなってしまうので『決めるところは決める!』って感じですね。

Type Aliasが保持されるようになった

Type Aliasが賢くなりました。

export type ValueTypes = number | string | boolean | bigint | RegExp;

export function mock(val: ValueTypes) {
    if (val === 'test') return null
    if (val === 10) return Symbol()

    return val;
}

この場合、unionにundefinedが加わり、mockの返り値は number | string | boolean | bigint | RegExp | Symbol | nullと評価されていたものがValueTypes | Symbol | nullに変わります。

4.2以前

mockの返り値はnumber | string | boolean | | bigint | RegExp | Symbol | nullと評価されていますね。

4.2以後

ValueTypes | Symbol | nullとかなりスマートに理解されていることがわかります。
また型から途中で生成されうる型を見抜くこともできるのでかなりコードの理解が捗ります。

このようにユニオンタイプの理解が代わり、エディターでドワーっと表示されていたところがスマートに表示されるようになりますね!!(@janus_welさん撮影方法アドバイスありがとうございます!)

テンプレート文字列がTemplate Literal Typesを持つようになった。

テンプレート文字列がTemplateLiteralTypesを持つようになります。以前のバージョンでは単なるstringとして評価されていたところがしっかりと評価されるようになります。
説明が難しいので、具体的にはコードを見てもらうのが一番ですね。

type SekaiType = '世界' | '現実';
// OK
const str1: `こんにちわ ${SekaiType}` = 'こんにちわ 世界'; 
// 4.2以前ではNG(template string = string)になってしまう。
const str2: `こんにちわ ${SekaiType}` = `こんにちわ ${'世界'}`; 

// OK: 4.2以上ではTemplate Literal、それ以外stringとして扱われる
const str3 = `こんにちわ ${'世界'}`; 
// なので4.2以前ではTemplateLiteralにstringを割り当てるのでNG
const str4: `こんにちわ ${SekaiType}` = str3; 

// letキーワードの場合に自動推論するとstringになるので注意
// この場合TemplateLiteralに向けてstringを代入しているのでNG
let str5 = `こんにちわ ${'世界'}`;
const str6: `こんにちわ ${SekaiType}` = str5;  // NG

// letキーワードも明示的にすればTemplateLiteralになるのでOK
let str7: `こんにちわ ${SekaiType}` = `こんにちわ ${'世界'}`;
const str8: `こんにちわ ${SekaiType}` = str7; // OK

// constキーワードなら自動推論させてもTemplateLiteralになるのでOK
const str9 = `こんにちわ ${'世界'}`;
const str10: `こんにちわ ${SekaiType}` = str9; // OK

想定通りに動くのでえええええええ・・・・って思うことも無くなりそうですね。

noPropertyAccessFromIndexSignatureの追加

ではまず以下のコードをみていきましょう。

interface Person {
    getFullName: () => string,
    firstName: string;
    lastName: string;
    [x: string]: any;
}

function greetPerson(person: Person) {
    // 定義済みプロパティへのアクセス
    const a = person.getFullName();
    // IndexSignatureへのアクセス
    const b = person.fullName();
}

この挙動がnoPropertyAccessFromIndexSignatureによって変わります。

     "noPropertyAccessFromIndexSignature": true, 

noPropertyAccessFromIndexSignatureをtrueにするとどうなるのか?というと

interface Person {
    getFullName: () => string,
    firstName: string;
    lastName: string;
    [x: string]: any;
}

function greetPerson(person: Person) {
    // 定義済みプロパティへのアクセスは通常通り
    const a = person.getFullName();
    // Property 'fullName' comes from an index signature, so it must be accessed with ['fullName'].
    const b = person.fullName();
    // indexSignatureへのアクセスはブラケット記法のみ許される
    const c = person['fullName']()
}

こちらはNGになります。つまり、定義されているプロパティにアクセスする時はドットアクセスのみが許容されるようになります。そのため、インデックスシグネチャで定義されているものについてはブラケットアクセスしないといけなくなります。

これはいいですね!!Pythonっぽくって僕は好きです。

// NGになる。
person.a = 10
// OK 
person['a'] = 10

つまり、定義されているものは全てドットアクセスになるので、プロパティの新規追加なのか上書きなのかとか定義されている(safeなもの)なのかどうなのか一目瞭然になるところも良いかなぁと思っています。

どのみち、インデックスシグネチャで定義されているものについてはエディタの補完も効かないので(効く方法があったらすみません)ドットアクセスしかつかいたくないモチベーションは特になく、コードを読むときに補足の情報があった方がより読みやすいなと感じています。ブラケットアクセスしている=
不明瞭なものなのか といった具合に。

abstract Construct Signature

abstractは以前からありましたが、abstract modifier(多分、abstract修飾子と訳すはず)が追加されパワーアップしました。これは結構難解でした・・・。
これを使うことによってabstractを使いやすくすることができます。

abstract class Entity {
    abstract id: number;
    abstract getId(): `UUID:${number}`;
}
abstract class ValueObject {
    abstract equal(tar: unknown): boolean;
}

以上二つのabstract classがあります。
これをフロントエンド用のものにカスタマイズするために、frontEndFactoryを作りたいと思います。
frontEndFactoryを通すことによって、localStorageを使って永続化をするためのpersistというメソッドが生えるようにします。
だけどもabstractの性質は外したくないので、拡張はするけどもabstractによって制限が守られるようにしてみます。
詳細な実装は後回しにしますが、今から実装するfrontEndFactoryをこんな感じに使っていきたいと思っているとします。

const F_Entity = frontEndFactory(Entity);
const F_ValueObject = frontEndFactory(ValueObject);

class Person extends F_Entity {
    constructor(public id: number) {
        super();
    }
    getId() {
        return `UUID:${this.id}`;
    }
}
const person = new Person(1);
person.persist();

class Age extends  F_ValueObject {
    constructor(public value: number) {
        super();
    }
    equal(age: Age) {
        return this.value === age.value;
    }
}
const age = new Age(20);
age.persist();

どちらもfrontEndFactoryを介して拡張されたabstract classをもとにクラスを作成しています。そのため、frontEndFactoryの引数は、ただのclassではなく、abstract classを想定しています。

この引数を実現するために使うのがabstract修飾子です。abstract修飾子はコンストラクターシグネチャの上に乗せることができます。
実装は適当ですが、abstract修飾子を使うことによってabstractクラスを受け継ぎながら、拡張することができます。

type Base<T> = abstract new (...arg: any[]) => T;
function frontEndFactory<T extends Base<object>>(base: T) {
    abstract class newClass extends base {
        persist() {
            localStorage.setItem('data', JSON.stringify(this))
        }
    }
    return newClass
}

ここでのポイントは、

type Base<T> = abstract new (...arg: any[]) => T;

ここにあります。これをすることによって、引数はabstractのconstractorという認識がなされるようになります。もしabstract修飾子を使わないで実装したのであれば、引数に指定された型と合わなくなるため下記のエラーに遭遇します。abstract classであってclassではないので当然ですね。

// Argument of type 'typeof Entity' is not assignable to parameter of type 'Base<object>'.
// Cannot assign an abstract constructor type to a non-abstract constructor type.(2345)
const F_Entity = frontEndFactory(Entity);
const F_ValueObject = frontEndFactory(ValueObject);

このように4.2ではabstract修飾子が追加され、使いやすくなりそうです。

--explainFiles フラグ

このフラグを使うことによって、typeScriptがみているファイルの名前とその理由を出力させることができます。
百聞は一件にしかずなので早速みてみましょう。

tsc --explainFiles

tsConfigでも大丈夫です。
このオプションをつけることによって出力されます。出力内容をみてみましょう。

$ tsc --explainFiles
node_modules/typescript/lib/lib.d.ts
  Default library for target 'es5'
node_modules/typescript/lib/lib.es5.d.ts
  Library referenced via 'es5' from file 'node_modules/typescript/lib/lib.d.ts'
node_modules/typescript/lib/lib.dom.d.ts
  Library referenced via 'dom' from file 'node_modules/typescript/lib/lib.d.ts'
node_modules/typescript/lib/lib.webworker.importscripts.d.ts
  Library referenced via 'webworker.importscripts' from file 'node_modules/typescript/lib/lib.d.ts'
node_modules/typescript/lib/lib.scripthost.d.ts
  Library referenced via 'scripthost' from file 'node_modules/typescript/lib/lib.d.ts'
index.ts
  Matched by include pattern '**/*' in 'tsconfig.json'
✨  Done in 1.06s.

このような結果が得られました。この結果はtypescriptに指定しているtargetによって変わります。今はes5指定しているのでこのようになります。

さて、ではここでtypesディレクトリを作成し、idex.d.tsとindex2.d.tsを作成し、typeRootにtypesディレクトリを作成します。

// index.d.ts
interface III {
    a: 10;
}
// index2.d.ts
interface III {
    a: 20;
    b: 20;
}

としています。
ではこの場合、aとbのどちらがエラーになるでしょう?
という答えがexplainFlagsの出力結果にあります。

// index.ts
const a: III = {
  a: 10,
  b: 20,
};
const b: III = {
  a: 20,
  b: 20,
}
$ tsc --explainFiles
....略
  Matched by include pattern '**/*' in 'tsconfig.json'
types/index.d.ts
  Matched by include pattern '**/*' in 'tsconfig.json'
types/index2.d.ts
  Matched by include pattern '**/*' in 'tsconfig.json'
✨  Done in 1.07s.

tsconfig/jsonにtypeRootを指定したのでin 'tsconfig.json'と出力されています。この場合、index.d.tsが先に読まれるのでエラーが発生したのはbの方でした!!

// index.ts
const a: III = {
  a: 10,
  b: 20,
};
// Error III.aは10でなければならない
// index.d.tsの宣言の方が優先される
const b: III = {
  a: 20,
  b: 20,
}

ではここでindex2.d.tsをandex2.d.tsに書き換えてみると・・・

types/andex2.d.ts
  Matched by include pattern '**/*' in 'tsconfig.json'
types/index.d.ts
  Matched by include pattern '**/*' in 'tsconfig.json'

andex2.d.tsの方が先に読まれるのでaの方でエラーが発生することになります。

// index.ts
// Error III.aは20でなければならない
// andex2.d.tsの宣言の方が優先される
const a: III = {
  a: 10,
  b: 20,
};
const b: III = {
  a: 20,
  b: 20,
}

このような形でtypescriptの気持ちを知ることのできるオプションでした。

型定義のないyieldはエラーになった

これもGoodJobですね!
いままで、ジェネレーターではyieldがうける値を指定しない場合はanyでした。
そのため正確な型定義をしているどうかに気づくことができないことが多々あったと思われます。
なので予想もしない引数がわたされると・・・・

ところが4.2からはそれらのコードは成敗されます。

function* ite() {
    // 型を指定していないのでエラー
    const res = yield 1
}

const gen = ite()
gen.next(10)

このようにそういった悪どいケースは成敗されるようになります。

const res: number = yield 1

のように型を明記するがGeneratorをつかって型を定義してあげる必要が今後はあります。

型のあるjsファイルのパース結果が変わった

一番ブレイキングだと思ったのがこれです。
とは言え影響がある人はそんなにいない気がします。
要はflowのようなjsファイルに型が書かれているようなファイルがあるときに、typescript APIをつかって構文解析をさせたときの結果が変わったという話です。
なので、flow to tsのようなことをtypescriptのAPIを使って行っている場合にのみ影響があります。

in演算子の右側にはPrimitive Typeが来れなくなった

より厳密になりましたが、大きく変わったわけではありません。
tsをやっていると何かとお世話になるin演算子の挙動が変わりました。

function env<T extends string>(str: T) {
  return 'test' in str
}
env('test')

ts4.2以前ではこの絶対に落ちるコードが許容されていましたが、4.2では The right-hand side of an 'in' expression must not be a primitive.のエラーが吐かれるようになりました。

終わりに

あと少しだけ書き忘れたことがあるのでそちらは後ほど追記させてください🥲

Discussion

magurotunamagurotuna

最後の「in演算子の右側にはPrimitive Typeしか来れなくなった」に関してですが、正しくは「Primitive Type 以外 しか」でしょうか?

ユタマこたろうユタマこたろう

おおおおお。。。。お二人ともその通りです!!
早速修正します!!

ご指摘ありがとうございます!!