同値型を判定する型

同値型を判定する型 | uraway

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直後の型A1A2が同値である必要があるということです。

具体的に値を入れて確認してみます。A1anyA2stringであるケース:

// @errors: 2322
declare let x: <A>() => A extends string ? 'assignable' : 'not assignable';
declare let y: <B>() => B extends any ? 'assignable' : 'not assignable';
// エラーが発生して代入できない
x = y;

A1A2がともに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

TypeScript 4.5 | uraway

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 では、個々の名前付きimporttype修飾子を付けることができるようになりました。

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 の最新バージョンを実行している限り、コンパイラWindowsMacOS のような一般的に大文字小文字を区別しない 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 コードには一切残らない。

そのため、型に対してtypeofinstanceofなどの JavaScript 上の値を扱う演算子を使うことはできない。

Playground Link

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演算子を使い、オブジェクトが固有のプロパティを持っているかどうかで判別するか:

Playground Link

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プロパティを持たせている。

Playground Link

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型を作成するため、次のコードは正しい:

Playground Link

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となる。

Playground Link

GitHub Issue

interface Person {
  name: string;
}
interface Lifespan {
  birth: Date;
  death?: Date;
}
type UnionKey = keyof (Person | Lifespan); // never
type IntersectionKey = keyof (Person & Lifespan); // "name" | "birth" | "death"

これは以下のコードが型エラーになるのと同じ理由

Playground Link

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;
}

オブジェクトは単一の厳密な型を作成しない

Handbook

Playground Link

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: stringage: 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"は起こらない。

Playground Link

interface Animal {
  name: string;
  age: number;
}

const animal = {
  name: "John",
  age: 22,
  address: "12345",
};

const obj: Animal = animal;

type エイリアスと interface の違い

  • interfaceUnion型を拡張できない。一度typeを経由する必要がある

Playground Link

interface Square {
  width: number;
}

interface Rectangle {
  height: number;
  width: number;
}

type TSquareOrRectangle = Square & Rectangle;

interface IShape extends TSquareOrRectangle {}
  • interfaceでは Named Tuple が表現できない

TypeScript のテストケースを探ったりコミッターに聞いてみたりしたができないっぽい

Handbook

Playground Link

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 のコンパイラオプションlibes2016を指定すると、"declaration merging"によって既存の型に新しい ECMA のメソッドが追加される。

混乱のもとになるので、巨大なライブラリの製作者でなければ、この機能はあまり使用しなくて良いと思われる。別の名前で個別に型を定義し、拡張することをおすすめする。

Playground Link

interface Rectangle {
  height: number;
}

interface Rectangle {
  width: number;
}

const rectangle: Rectangle = {
  height: 200,
  width: 100,
};

ECMAScript にない言語機能

Enum

Enum は結構癖が強い。通常のenumJavaScript オブジェクトに変換されるのに対して、const enumは TypeScript の型システム上でしか存在しない。

Playground Link

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"を受け入れても良さそうだが、型エラーが発生する:

Playground Link

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 を使ったほうが良い:

image

Playground Link

type FlavorUnion = "vanilla" | "chocolate" | "strawberry";

let flavor: FlavorUnion = "vanilla";

TypeScript のソースコードを読んでみると、ビットによるフラグ管理に Enum が使われていることが分かる。おそらくはこのために Enum が導入されたのだろう。

ビット演算子を使うと複数のメンバを持つメンバがかなり書きやすい。

Playground Link

// ビット演算時、数値は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"ではなく、文字列型として推論されるため型エラーが発生する。

Playground Link

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により文字列リテラル型として変数を宣言する:

Playground Link

type Name = "Alice" | "Bob";

declare function setName(name: Name): void;

let alice: Name = "Alice";
setName(alice);

const bob = "Bob";
setName(bob);

type-narrowing

Handbook

control flow analysisのおかげで、型チェッカーが変数の処理を追ってくれるので、型制限を維持したまま、変数に「その時点でもっともあり得る型」が割り当てられる。

Playground Link

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)が導入されればやりようはありそうな気がする。

GitHub Issue

Playground Link

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をお手軽に試したいと思ったので作りました

github.com

コードスニペットを選択して拡張のメニューを押すと、そのコードを貼り付けたTS playgroundが開く

image

拡張のアイコンをクリックすると、TSが実行できるポップアップが開くのでGitHubで見かけたコードとかのちょっとした型の確認とかに使う

image

ストアに登録はしてないのでインストールには手間がかかる。

  1. リリースページからdist.zipファイルをダウンロードして解凍する
  2. ブラウザのアドレスバーに"chrome://extensions"と打ち込み、拡張機能ページを開く
  3. 「パッケージ化されていない拡張機能を読み込む」をクリックして解答したフォルダを選択する

TypeScriptでもジェネリクスをインスタンス化したい

C# だとジェネリクスの型パラメータは実行時にも利用できる

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()