TypeScript Websiteの翻訳しませんか?
同値型を判定する型
ref: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650
ある程度型パズルに慣れている方は、型同士が同じかどうか判定する型と聞いて次のように思いつくのではないでしょうか:
type Equals<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false;
しかし、これは assignability(代入可能かどうか)だけを判定しているため、any
型に対してはうまく動作しません。
type Equals<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false; // should be true, got true type test01 = Equals<string, string>; // ^? // should be false, got false type test02 = Equals<{ foo: string }, { bar: string }>; // ^? // should be false, but got true type test03 = Equals<any, { bar: string }>; // ^?
型が全くの同値であるかを判定するには、「条件付き型同士が割り当て可能になるにはextends
直後の型どうしが同値でなければならない」というチェッカーの性質を利用します。
export type Equals<A1 extends any, A2 extends any> = (<A>() => A extends A2 ? 'assignable' : 'not assignable') extends <B>() => B extends A1 ? 'assignable' : 'not assignable' ? true : false; // should be true, got true type test01 = Equals<string, string>; // ^? // should be false, got false type test02 = Equals<{ foo: string }, { bar: string }>; // ^? // should be false, got false type test03 = Equals<any, { bar: string }>; // ^?
詳細に見るために、Equals
を分解してみます。
declare let x: <A>() => A extends A2 ? 'assignable' : 'not assignable'; declare let y: <B>() => B extends A1 ? 'assignable' : 'not assignable'; x = y;
x に y が割り当て可能(代入可能)なとき、Equals
の戻り値はtrue
であるということが言えます。
参考にした Issue のコメントでは、次のように述べられていました:
Here's a solution that makes creative use of the assignability rule for conditional types, which requires that the types after extends be "identical" as that is defined by the checker:
したがって、上記コードにおいて、条件付き型 x に条件付き型 y が割り当て可能であるためには、extends
直後の型A1
とA2
が同値である必要があるということです。
具体的に値を入れて確認してみます。A1
がany
、A2
がstring
であるケース:
// @errors: 2322 declare let x: <A>() => A extends string ? 'assignable' : 'not assignable'; declare let y: <B>() => B extends any ? 'assignable' : 'not assignable'; // エラーが発生して代入できない x = y;
A1
とA2
がともにstring
であるケースでは:
declare let x: <A>() => A extends string ? 'assignable' : 'not assignable'; declare let y: <B>() => B extends string ? 'assignable' : 'not assignable'; // 代入可能 x = y;
ではなぜ「条件付き型同士が割り当て可能になるにはextends
直後の型どうしが同値でなければならない」という性質があるのでしょうか。
extends
直後の型が同値でなくても、条件付き型同士が割り当て可能だと仮定してみます:
declare let x: <A>() => A extends string ? 'assignable' : 'not assignable'; declare let y: <B>() => B extends number ? 'assignable' : 'not assignable'; const x_1 = x<string>(); // ^? const y_1 = y<string>(); // ^? // @ts-ignore x = y; // 関数シグネチャの定義から戻り値はx_2は`assignable`のはず // しかし、xにyを代入しており、y_1の戻り値は`not assignable`なので // 戻り値は`assignable | not assignable`のユニオン型でなければ矛盾する const x_2 = x<string>(); // ^?
インターセクション型と通常の型は同値とみなされない
{ foo: true } & { bar: false }
と{ foo: true; bar: false }
は同値とみなされないことには注意が必要です。
type X1 = { foo: true } & { bar: false }; type X2 = { foo: true; bar: false }; export type Equals<A1 extends any, A2 extends any> = (<A>() => A extends A2 ? 'assignable' : 'not assignable') extends <B>() => B extends A1 ? 'assignable' : 'not assignable' ? true : false; // should be true, but got false type test01 = Equals<X1, X2>; // ^?
これが意図したものであるかは不明ですが、同値かどうか比較している部分のソースコードを見ると、フラグ(flags
)を比較していることが分かります。
// https://raw.githubusercontent.com/microsoft/TypeScript/main/src/compiler/checker.ts function isTypeRelatedTo( source: Type, target: Type, relation: ESMap<string, RelationComparisonResult> ) { // ... if (relation !== identityRelation) { // ... } else { if (source.flags !== target.flags) return false; // ... } // ... }
フラグの定義(TypeFlags
)を見てみると、インターセクション型のフラグとオブジェクト型のフラグが異なるため、同値とみなされないようです。
TypeScript 4.5
ref: https://devblogs.microsoft.com/typescript/announcing-typescript-4-5/
ECMAScript モジュールサポートの延期
当初 TypeScript 4.5 beta では、ECMAScript モジュールをサポートするオプションがあったが、現在はナイトリーリリースのみで利用可能。
Awaited
型の追加
4.5 では、Awaited
型が追加されました。この型は、非同期関数の await や、Promise の.then()
メソッドをモデル化するものです。
// A = string type A = Awaited<Promise<string>>; // B = number type B = Awaited<Promise<Promise<number>>>; // C = boolean | number type C = Awaited<boolean | Promise<number>>;
node_modules による lib のサポート
TypeScript は一般的な宣言ファイル(JavaScript で利用可能な API や DOM API)をバンドルしていますが、TypeScript をアップグレードすると、これらの変更にも対応しなければなりません。
4.5 では、package.json
にバージョンを指定することで、特定のビルトイン宣言ファイルをオーバーライドできます。
{ "dependencies": { "@typescript/lib-dom": "npm:@types/web" } }
4.5 以降は、TypeScript をアップグレードしても依存ファイルのバージョンの宣言ファイルが使用されます。
識別記号としてのテンプレート文字列
4.5 では、テンプレート文字列型を識別記号として認識できるようになりました。
export interface Success { type: `${string}Success`; body: string; } export interface Error { type: `${string}Error`; message: string; } export function handler(r: Success | Error) { if (r.type === 'HttpSuccess') { // 'r'はSuccess型となり、 // `body`型はstringとなる let token = r.body; } }
es2022 モジュールのサポート
トップレベル await などの機能を持つ、es2022
をサポートしています。
{ "compilerOptions": { "module": "es2022" } }
条件付き型における末尾呼び出しの除去
TypeScript では、無限に続く可能性のある再帰や、時間がかかってエディタの操作に影響があるような型の拡張を検出したときに、潔く失敗する必要があることがあります。そのため、無限に深い型を分解しようとするときや、多くの中間結果を生成する型を扱うときに、暴走しないようにするヒューリスティックを備えています。
// @errors: 2589 type InfiniteBox<T> = { item: InfiniteBox<T> }; type Unpack<T> = T extends { item: infer U } ? Unpack<U> : T; type Test = Unpack<InfiniteBox<number>>;
例えば、下記のTrimLeft
は、一つの分岐で末尾再帰的になるように書かれています。呼び出しごとにスタックに呼び出し元に戻るための情報を保存する必要がなく、末尾呼び出しの最適化(末尾呼び出しの除去)が行えます。
type TrimLeft<T extends string> = T extends ` ${infer Rest}` ? TrimLeft<Rest> : T; // 4.4以前はエラー: Type instantiation is excessively deep and possibly infinite. type Test = TrimLeft<' oops'>;
インポート削除の無効化
TypeScript では、インポートを使用していることが検知できない場合があります
// @preserveValueImports: true import { Animal } from './animal.js'; eval('console.log(new Animal().isDangerous())'); // @filename: ./animal.js export class Animal() {}
デフォルトでは、このようなインポートは常に削除されます。4.5 では、--preserveValueImports
フラグを使ってインポートを削除しないようにできます。
インポート名のtype
修飾子
4.4 以前にも、インポートを削除することができる印としてtype
修飾子がありますが、値もインポートしたい場合、同じモジュール名に対して 2 つの import
文が必要でした。
import { someFunc } from './some-module.js'; import type { BaseType } from './some-module.js'; export class Thing implements BaseType {} // @filename: some-module.js export function someFunc() {} export class BaseType {}
4.5 では、個々の名前付きimport
にtype
修飾子を付けることができるようになりました。
import { someFunc, type BaseType } from "./some-module.js"; export class Thing implements BaseType {} // @filename: some-module.js export function someFunc() {} export class BaseType {}
プライベートフィールドの存在チェック
4.5 では、オブジェクトがプライベートフィールドを持っているかチェックする ECMAScript のプロポーザルをサポートしています。
class Person { #name: string; constructor(name: string) { this.#name = name; } equals(other: unknown) { return other && typeof other === "object" && #name in other && // <- this is new! this.#name === other.#name; } }
インポートアサーション
4.5 では、ECMAScript によるインポートアサーションのプロポーザルをサポートしています。これはランタイム時にインポートが期待されるフォーマットを持っているかどうかを確認するために使用する構文です。
// @resolveJsonModule: true import obj from "./something.json" assert { type: "json" }; // @filename: something.json {}
JSDoc における const アサーションとデフォルト型引数
4.5 では、にいくつかの JSDoc 表現が加わりました。
その一つの例が const アサーションです。TypeScript では、リテラルの後に const を書くことでより正確で不変的な型を表現することができます。
let a = { prop: 'hello' }; // ^? let b = { prop: 'hello' } as const; // ^?
JS ファイルでも、JSDoc のタイプアサーションを使って同じことができるようになりました。
let a = { prop: 'hello' }; let b = /** @type {const} */ { prop: 'hello' };
4.5 では、さらにデフォルト型引数が JSDoc に追加されました。
type Foo<T extends string | number = number> = { prop: T };
JavaScript では@typedef
宣言を使って次のように書くことができます
/** * @template {string | number} [T=number] * @typedef Foo * @property prop {T} */ // or /** * @template {string | number} [T=number] * @typedef {{ prop: T }} Foo */
realpathSync.native によるロードタイムの高速化
TypeScript は、すべての OS で Node.js の realpathSync.native 関数を利用するようになりました。
以前はこの関数は Linux でのみ使用されていましたが、TypeScript 4.5 では、Node.js の最新バージョンを実行している限り、コンパイラは Windows や MacOS のような一般的に大文字小文字を区別しない OS でもこの関数を使用します。この変更により、Windows 上の特定のコードベースにおいて、プロジェクトの読み込みが 5-13%速くなりました。
TypeScript 4.4
TypeScript 4.4
https://devblogs.microsoft.com/typescript/announcing-typescript-4-4/
代入された判別式の control flow analysis
以前の TypeScript では、type guard の判別式を変数に代入した場合、その判別式は機能しない。
function foo(arg: unknown) { const argIsString = typeof arg === "string"; if (argIsString) { console.log(arg.toUpperCase()); // ~~~~~~~~~~~ // Error! Property 'toUpperCase' does not exist on type 'unknown'. } }
TypeScript 4.4 では、変数をチェックし、内容が判別式であれば変数の型を絞り込むことができる。
シンボルとテンプレート文字列としての Index Signature
TypeScript 4.4 からはシンボルとテンプレート文字列パターンが Index Signature として使用できる
interface Colors { [sym: symbol]: number; } const red = Symbol("red"); let colors: Colors = {}; colors[red] = 255;
interface Options { width?: number; height?: number; // 任意の`data-`から始まる文字列をプロパティのキーとして使用できる [optName: `data-${string}`]: unknown; } let option: Options = { width: 100, height: 100, "data-blah": true };
useUnknownInCatchVariables
オプション
catch
句の変数の型をany
からunknown
に変更するオプション
try { } catch (err) { // unknown }
strict
オプション下では自動的にオンになる。
exactOptionalPropertyTypes
オプション
多くの JavaScript コードでは、存在しないプロパティとundefined
を値として持つプロパティは同一のものとして扱われる。そのため TypeScript でも、オプショナルプロパティは、undefined
とのユニオンタイプとして考えられてきた。例えば、
interface Person { name: string; age?: number; }
これは下記と同様:
interface Person { name: string; age?: number | undefined; }
exactOptionalPropertyTypes
オプション下では、オプショナルプロパティはundefined
とのユニオンタイプを追加しない。存在しないプロパティと区別される。
const p: Person = { name: "Daniel", age: undefined, // Type 'undefined' is not assignable to type 'number'.(2322) };
static ブロック
ECMAScript プロポーザルの static ブロックをサポート:
class Foo { static count = 0; // This is a static block: static { if (someCondition()) { Foo.count++; } } }
TypeScript の陥りやすい罠
TypeScript の陥りやすい罠
Oreilly の Effective Typescript を読んだ。説明が簡潔でわかりやすく、章の構成も読みたいところだけ読めば良いようになっており、すらすら読める。対象読者層はある程度 TypeScript を使っており、ひと通りの機能を触ったことがある人だと思う。全くの初心者はまず Handbookを一通り眺めてみることをおすすめする。本書籍はTypeScript本の一冊目として手に取るものではない。
2 年くらい TypeScript を使っているが意外と詳しく知らない動作やうろ覚えだった機能についても書かれており、参考になった。Conditional TypesやUtility Typesについては薄めだったのは残念。
本書を読んだ上で、Handbook で気になる部分を再度読み直したり、TypeScript リポジトリの Issue を読むことで浮かび上がってきた、 TypeScript を使う際に気をつけるべき点、他の静的型言語とは異なる点をいくつかまとめておく。(なので本書に載っていないものもある)
動作確認は TypeScript v4.1.5 で行った。
削除される型
TypeScript の型情報は JavaScript コードには一切残らない。
そのため、型に対してtypeof
やinstanceof
などの JavaScript 上の値を扱う演算子を使うことはできない。
interface Square { width: number; } interface Rectangle { height: number; width: number; } function getArea(shape: Square | Rectangle) { if (typeof shape === Square) { // 'Square' only refers to a type, but is being used as a value here.(2693) } if (shape instanceof Rectangle) { // 'Rectangle' only refers to a type, but is being used as a value here.(2693) } }
上記コードのように Union 型のメンバを判別したい場合は、in
演算子を使い、オブジェクトが固有のプロパティを持っているかどうかで判別するか:
interface Square { width: number; } interface Rectangle { height: number; width: number; } function getArea(shape: Square | Rectangle) { if ("height" in shape) { shape; // (parameter) shape: Rectangle } else { shape; // (parameter) shape: Square } }
"タグ"付きの Union として Union のメンバに共通するプロパティをもたせることで判別する。下記の例では、kind
プロパティを持たせている。
interface Square { kind: "square"; width: number; } interface Rectangle { kind: "rectangle"; height: number; width: number; } function getArea(shape: Square | Rectangle) { if (shape.kind === "square") { shape; // (parameter) shape: Square } else { shape; // (parameter) shape: Rectangle } }
ちなみにclass
構文は、同時にinterface
型を作成するため、次のコードは正しい:
class Square { constructor(public width: number) { this.width = width; } } class Rectangle { constructor(public width: number, public height: number) { this.width = width; this.height = height; } } function getArea(shape: Square | Rectangle) { if (shape instanceof Square) { shape; // (parameter) shape: Square } else { shape; // (parameter) shape: Rectangle } }
keyof Union は never 型
keyof
は型にアクセス可能なプロパティのキーを返す。共通のプロパティを持たない型同士の Union であれば、その型にアクセス可能なプロパティキーは存在しないので、never
となる。
interface Person { name: string; } interface Lifespan { birth: Date; death?: Date; } type UnionKey = keyof (Person | Lifespan); // never type IntersectionKey = keyof (Person & Lifespan); // "name" | "birth" | "death"
これは以下のコードが型エラーになるのと同じ理由
interface Person { name: string; } interface Lifespan { birth: Date; death?: Date; } declare const personOrLifespan: Person | Lifespan; if (personOrLifespan.name) { // Property 'name' does not exist on type 'Person | Lifespan'. Property 'name' does not exist on type 'Lifespan'. }
personOrLifespan
の型は、以下の型 ではない
type PersonOrLifespan = { name?: string; birth?: Date; death?: Date; }
オブジェクトは単一の厳密な型を作成しない
interface Person { name: string; age: number; address: string; } interface Animal { name: string; age: number; } function callAnimal(animal: Animal) { console.log(`${animal.name}`); } const person: Person = { name: "John", age: 22, address: "12345" }; callAnimal(person); // Animal型の引数にPerson型のオブジェクトを割り当てることができる
TypeScript のオブジェクトは単一の厳密な型を作成しない。構造的型システムである TypeScript において、オブジェクトの形状の一部分が型の形状と一致すれば、そのオブジェクトは型に割り当てることができると判断される。
上記コードでいえば、name: string
とage: number;
のプロパティを持つオブジェクトはAnimal
型でもあるとみなされる。
"exccess property checking"はオブジェクトリテラルの作成時にしか発生しない
次のコードは、構造的型システムの観点からすれば正しいように思える。animal
オブジェクトはAnimal
型に必要なすべてのプロパティを持っているし、プロパティの値の型も間違っていない。しかし、TypeScript は型エラーを警告してくれる。
interface Animal { name: string; age: number; } const animal: Animal = { name: "John", age: 22, address: "12345", // Type '{ name: string; age: number; address: string; }' is not assignable to type 'Animal'. Object literal may only specify known properties, and 'address' does not exist in type 'Animal'. };
これは"exccess property checking"と呼ばれるチェック機能によるもので、オブジェクトリテラルを作成したときのみに動作し、不要なプロパティの存在やプロパティ名をタイプミスしていないか検証してくれる。
以下のコードでは"exccess property checking"は起こらない。
interface Animal { name: string; age: number; } const animal = { name: "John", age: 22, address: "12345", }; const obj: Animal = animal;
type エイリアスと interface の違い
interface
はUnion
型を拡張できない。一度type
を経由する必要がある
interface Square { width: number; } interface Rectangle { height: number; width: number; } type TSquareOrRectangle = Square & Rectangle; interface IShape extends TSquareOrRectangle {}
interface
では Named Tuple が表現できない
TypeScript のテストケースを探ったりコミッターに聞いてみたりしたができないっぽい
type TLocation = [lat: number, long: number]; // [lat: number, long: number] interface ILocation extends Array<number> { 0: number; 1: number; length: 2; } const location1: TLocation = [1, 2]; location1[0]; // (property) 0: number (lat) const location2: ILocation = [1, 2]; location2[0]; // (property) ILocation[0]: number
interface
は"declaration merging"が可能
interface
の型宣言を結合する。例えば TypeScript のコンパイラオプションlib
にes2016
を指定すると、"declaration merging"によって既存の型に新しい ECMA のメソッドが追加される。
混乱のもとになるので、巨大なライブラリの製作者でなければ、この機能はあまり使用しなくて良いと思われる。別の名前で個別に型を定義し、拡張することをおすすめする。
interface Rectangle { height: number; } interface Rectangle { width: number; } const rectangle: Rectangle = { height: 200, width: 100, };
ECMAScript にない言語機能
Enum
Enum は結構癖が強い。通常のenum
は JavaScript オブジェクトに変換されるのに対して、const enum
は TypeScript の型システム上でしか存在しない。
enum FlavorEnum { VANILLA = 1, CHOCOLATE = 2, STRAWBERRY = 99, } let flavorEnum = FlavorEnum.CHOCOLATE; FlavorEnum[0]; // <- indexでアクセスできてしまう const enum FlavorConstEnum { VANILLA = 1, CHOCOLATE = 2, STRAWBERRY = 99, } let flavorConstEnum = FlavorConstEnum.CHOCOLATE; FlavorConstEnum[0]; // A const enum member can only be accessed using a string literal.(2476)
文字列を割り当てる(string enum
)こともできるが、これは構造的型システムの TypeScript の中でも特殊なふるまいになる。構造的型システムからすれば下記のflavor
は文字列リテラル型の"strawberry"
を受け入れても良さそうだが、型エラーが発生する:
enum FlavorEnum { VANILLA = "vanilla", CHOCOLATE = "chocolate", STRAWBERRY = "strawberry", } let flavor = FlavorEnum.CHOCOLATE; flavor = "strawberry"; // Type '"strawberry"' is not assignable to type 'FlavorEnum'.(2322)
下記画像のように Enum のメンバにコメントを付けたい、とかでなければ文字列リテラルの Union を使ったほうが良い:
type FlavorUnion = "vanilla" | "chocolate" | "strawberry"; let flavor: FlavorUnion = "vanilla";
TypeScript のソースコードを読んでみると、ビットによるフラグ管理に Enum が使われていることが分かる。おそらくはこのために Enum が導入されたのだろう。
ビット演算子を使うと複数のメンバを持つメンバがかなり書きやすい。
// ビット演算時、数値は32ビットの整数値に変換される const enum Flavor { /** 1 (32ビットの整数値: 0000 0000 0000 0000 0000 0000 0000 0001) */ VANILLA = 1 << 0, /** 2 (32ビットの整数値: 0000 0000 0000 0000 0000 0000 0000 0010) */ CHOCOLATE = 1 << 1, /** 4 (32ビットの整数値: 0000 0000 0000 0000 0000 0000 0000 0100) */ STRAWBERRY = 1 << 2, /** 7 (32ビットの整数値: 0000 0000 0000 0000 0000 0000 0000 0111) */ All = VANILLA | CHOCOLATE | STRAWBERRY, } function order(flavor: Flavor) { if (flavor & Flavor.VANILLA) { throw new Error("VANILLA is out of stock!"); } } let chocolate = Flavor.CHOCOLATE; order(chocolate); let all = Flavor.All; order(all); // Error: VANILLA is out of stock! let vanilla = Flavor.VANILLA; order(vanilla); // Error: VANILLA is out of stock!
コンパイルされた JS コードにも全く無駄がない。ソースコードの圧縮にも一役買っているようだ:
"use strict"; function order(flavor) { if (flavor & 1 /* VANILLA */) { throw new Error("VANILLA is out of stock!"); } } let chocolate = 2 /* CHOCOLATE */; order(chocolate); let all = 7 /* All */; order(all); // Error: VANILLA is out of stock! let vanilla = 1 /* VANILLA */; order(vanilla); // Error: VANILLA is out of stock!
複雑なEnumを持たない通常のアプリ規模では必要ないテクニックだが、置き換えができないケースもあることは知っておくべきだろう。
Decorator
experimentalDecorators
オプションを指定すれば TypeScript でも Decorator は使用できるが、避けるべき。TC39でも仕様は定まっていない。加えて TypeScript で使用できる Decorator の実装は 2014 年の提案のものであり、将来的に実装に破壊的変更が入る可能性が高い。
Angular や NestJS では当たり前のように Decorator を使っているが、対応に追われそうだ。
type-widening
TypeScript の型の推論はとても賢く、いちいち型注釈を付けなくても型が付くという安心を得られる。しかしそれゆえに、混乱を引き起こすケースがある。
次のケースでは、alice
変数は文字列リテラル型である"Alice"
ではなく、文字列型として推論されるため型エラーが発生する。
type Name = "Alice" | "Bob"; declare function setName(name: Name): void; setName("Alice"); let alice = "Alice"; // let alice: string setName(alice); // Argument of type 'string' is not assignable to parameter of type 'Name'.(2345)
これを解消するには、型注釈を使う、あるいはconst
により文字列リテラル型として変数を宣言する:
type Name = "Alice" | "Bob"; declare function setName(name: Name): void; let alice: Name = "Alice"; setName(alice); const bob = "Bob"; setName(bob);
type-narrowing
control flow analysis
のおかげで、型チェッカーが変数の処理を追ってくれるので、型制限を維持したまま、変数に「その時点でもっともあり得る型」が割り当てられる。
function foo(x: string | number | boolean): number | boolean { if (typeof x === "string") { x; // (parameter) x: string x = 1; x; // (parameter) x: number // x = {}; // ← xはもともと string | number | boolean のUnion型なので、オブジェクトは代入できない Type '{}' is not assignable to type 'string | number | boolean'. } return x; // (parameter) x: number | boolean }
ただしunknown
に対しては、うまく動作しない。否定型(Negated Type
)が導入されればやりようはありそうな気がする。
function foo(x: unknown): unknown { if (x == null) { x; // (parameter) x: unknown (実際は null | undefined) x = 1; x; // (parameter) x: unknown (実際は数値) } else if (isObject(x)) { x; // (parameter) x: object | null (実際はオブジェクト) x = 2; x; // (parameter) x: unknown } return x; // (parameter) x: unknown } function isObject(x: unknown): x is object { return typeof x === "object"; }
JavaScript のスーパーセットであるということ
動作するあらゆる JavaScript は、(型エラーが発生するかどうかに関わらず)TypeScript コンパイラを通しても JavaScript に変換され、変わらず動作する。
TypeScript の原理原則として、JavaScript のランタイム動作を決して変更しないというものがある。また一部型システムにおいてもその動作を尊重している。
例えば、以下のコードは TypeScript として正しい。JavaScript では文字列と数値を加算演算子でつなぐと、数値は文字列に型強制される。
TypeScript はこのふるまいを尊重するため型チェッカーはエラーを発生させない。
const x = "1" + 1; // "11" const y = 1 + "1"; // "11"
しかし、TypeScript はどこかで線引をする。以下のコードは JavaScript の構文的に正しく、ランタイムエラーを発生させないが、TypeScript の型チェッカーは警告を表示する。
const x = 1 + []; // Operator '+' cannot be applied to types 'number' and 'never[]'.(2365) // JavaScriptでは、結果は 1 const y = 1 + null; // Object is possibly 'null'.(2531) // JavaScriptでは、結果は 1
線引するポイントは、こうしたコードをプログラマが意図的に書いているかよりもバグの可能性が高いと思われるかどうかだろう。具体的には、配列やnull
を数値に加算するコードはプログラマが意図しないもの(バグ)であると TypeScript のコミッターが判断しているのだと思う。
TSをブラウザで実行するためのChrome拡張作った
ブラウザ上でTypeScriptをお手軽に試したいと思ったので作りました
コードスニペットを選択して拡張のメニューを押すと、そのコードを貼り付けたTS playgroundが開く
拡張のアイコンをクリックすると、TSが実行できるポップアップが開くのでGitHubで見かけたコードとかのちょっとした型の確認とかに使う
ストアに登録はしてないのでインストールには手間がかかる。
TypeScriptでもジェネリクスをインスタンス化したい
using System; var factory = new Factory(); factory.LogType<Car>(); // Car class Car { string color = "red"; } public class Factory { public void LogType<T>() { Console.WriteLine(typeof(T).Name); } }
TypeScriptによる型はコンパイル時にはすべて取り除かれるため、実行時には利用できない。(typeof
で型は取り出せない。)そのためメソッドの引数から推論させ、それを利用する
class Car { color = "red"; } class Factory { logType<T>(type: (new () => T)): void { console.log(JSON.stringify(new type())) } } let factory = new Factory(); factory.logType(Car); // "{"color":"red"}"
TypeScriptは構造的型付けなので、型は構造で表現される。いわゆるダックタイピング
構造的型付けは、一見して直感には反するかもしれない。例えば以下のコードはエラーにならない。Car
クラスはEmpty
クラスのプロパティをすべて持っているとみなされるため
class Empty {} class Car { color = 'red'; } let empty: Empty = {}; empty = new Car()