RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

TypeScript 入門

f:id:tech-rakus:20201223223206p:plain

こんにちは。新卒のid:w1pと申します。
今回業務でTypeScriptを導入するということで、いい機会なのでTypeScriptについていろいろ調べました。

目次

環境構築

Microsoft公式がブラウザ上で実行できるサンドボックスを提供しており、これを使用すると手軽にTypeScriptを試すことができます。
TypeScriptからJavaScriptへの変換結果や、型定義ファイル生成の結果まで見れます。
この記事で出てくるサンプルコードはすべてTS Playground v4.1.2上で行っています。

Visual Studio CodeIntellij IDEAなど手元のエディタで書いてローカルで実行したい場合には以下の手順を踏む必要があります。
npm or yarnがインストールされていることが前提です。

Step1. TypeScriptをインストールする

$ mkdir ts && cd ts
$ npm init -y # yarn init -y
$ npm i -D typescript # yarn add -D typescript

Step2. .ts拡張子のファイルを作成する

$ echo 'console.log("Hello, TypeScript!");' > ./hello.ts

Step3. TypeScriptを実行する
ここでは手軽にTypeScriptを実行する方法として、tscを使用する方法と、ts-nodeというパッケージを使用する方法を紹介します。

tscを使用する場合
tscはTypeScriptをJavaScriptへトランスパイルするための公式ツールです。 tscの挙動はtsc実行時にオプションとして渡したり、tsconfig.jsonというファイルで変更することができます。
hello.tsのあるフォルダで以下を実行します。

$ npx tsc ./hello.ts # yarn run tsc ./hello.ts

tscはトランスパイルした結果を指定されたディレクトリに出力します。
今回は特に指定していませんのでカレントディレクトリに出力されます。

あとは通常のjsファイルと同様に実行できます。

$ node hello.js
Hello, TypeScript!

ts-nodeを使用する場合
ts-nodeを使用すると、tscnodeという2度手間を省略できます。

$ npm i -D ts-node # yarn add -D ts-node

実行は以下のように行えます。

$ npx ts-node ./hello.ts # yarn run ts-node ./hello.ts

TypeScriptの基本的な文法

ここまではどのようにTypeScriptファイルを実行するのかについて見てきました。
ここからは基本的なTypescriptの文法について見ていきます。

アノテーションの書き方

アノテーションとは、変数や関数の戻り値などの型が何であるかを明示的に指定することです。
基本的には: typeの形式で書くことができます。

// 変数nの型をnumberに
let n: number = 123
n = 'string' // Type 'string' is not assignable to type 'number'.

// argをstring, fの戻り値はvoidに
function f(arg: string): void { /* */ }
f(123) // Argument of type 'number' is not assignable to parameter of type 'string'.

TypeScriptは型推論(型をコンテクストから推論すること)により、全ての場所に型アノテーションを必要としない仕組みになっています。

// nはnumber型と推論される
let n = 123

// tはboolean型と推論される
let t = true;

基本の型

早速ですが、TypeScriptの核となる型についてみていきます。 ここで紹介する型はプリミティブ型と呼ばれる基本的な型です。

number型

number型は実数に加え、無限を表すInfinity, 非数を表すNaNが含まれます。

const n1: number = 123;
const n2: number = -0.1;
const n3: number = Infinity;
const n4: number = NaN;

bigint型

ES2020で導入されたbigint型はNumber.MAX_SAFE_INTEGER(2^53 - 1)よりも大きな整数を扱うことができます。 精度が落ちる可能性があるためnumber型変数へ直接代入することはできず、明示的な変換が必要になります。

let big = 123n;
let num = 123;

num = big; // Type 'bigint' is not assignable to type 'number'.
big = num; // Type 'number' is not assignable to type 'bigint'.

num = Number(big);
big = BigInt(num);

string型

文字列全般をとりうる型です。

const s1: string = "Hello, World!";
const s2: string = s1.slice(0, 5);

boolean型

truefalseの2つの値をとり得る型です。

let b: boolean;
b = true;
b = false;
b = !!1;
b = 1; // Type 'number' is not assignable to type 'boolean'.

symbol型

ES2015から取り入れられた、一意の値を生成するための仕組みです。

const s1: unique symbol = Symbol('abc'); // typeof s1
let s2 = Symbol('abc'); // symbol
let s3 = Symbol.for('abc'); // symbol
const s4 = Symbol.for('abc') // symbol
console.log(s1 === s2); // false
console.log(s2 === s3); // false
console.log(s3 === s4); // true

null型

nullもしくはany型のみを受け付ける型です。

const n1: null = null;
const n2: null = 123 as any;
const n3: null = undefined; // Type 'undefined' is not assignable to type 'null'.
const n4: null = 123 as unknown; // Type 'unknown' is not assignable to type 'null'.

undefined型

undefinedとany型のみを受け付けます。

const u1: undefined = undefined;
const u2: undefined = 123 as any;
const u3: undefined = null; // Type 'null' is not assignable to type 'undefined'.
const u4: undefined = 123 as unknown; // // Type 'unknown' is not assignable to type 'null'.

エイリアスで型に別名をつける

エイリアスと呼ばれる機能により、任意の型に別名をつけることができます。 型エイリアスtypeキーワードで作成できます。

type ID = number;
type Name = string;
type Cond = boolean;

リテラル

TypeScriptの面白い特徴としてリテラル型があります。
リテラル型とは、プリミティブ型に含まれる一部の値のみをとりうるような型です。
constで変数を定義する際に、TypeScriptはその値はもう変化しないだろうと考えリテラル型で推論します。

// numberのリテラル型
let num1 = 123; // number型
const num2 = 123; // 123型

// stringのリテラル型
let str1 = "abc"; // string型
const str2 = "abc"; // "abc"型

// booleanのリテラル型
let bool1 = true; // boolean型
const bool2 = false; // false型

後述するUnion型とよく組み合わせて使われます。

// 許容するHTTPメソッドの一覧
type AllowedHttpMethods = "GET" | "HEAD" | "POST" | "PUT";
let method: AllowedHttpMethods = "GET"; // OK
method = "PUT"; // OK
method = "DELETE"; // Type '"DELETE"' is not assignable to type 'AllowedHttpMethods'.

複合型

object型

プリミティブ型以外の値をとりうる型です。
オブジェクトの構造を指定することはできません。

const o1: object = { a: 123, b: "abc" };
const o2: object = {}
const o3: object = 123; // Type 'number' is not assignable to type 'object'.
const o4: object = null; // Type 'null' is not assignable to type 'object'.

array型

配列を表す型です。
アノテーションの方法としてT[]Array<T>の2パターンありますが、前者の方が多く使われています。

const arr1: number[] = [1, 2, 3];
const arr2: Array<string> = ["a", "b", "c"];

tuple型

TypeScriptではタプル型を宣言できます。
タプルは複数の要素からなる組み合わせのような型です。
配列と違うのは、異なる型の要素が共存できる点です。

const tup1: [number, string] = [1, "str"];
const tup2: [number, number, number] = [1, 2, 3];
const arr: number[] = tup2; // number型の要素しかないため配列に割り当て可能

enum

tuple同様、TypeScriptは独自にenumを使用できます。 enumプログラマが事前に定義した特定の値のみをとりうるような型です。

enum Suits {
    Diamonds,
    Clubs,
    Hearts,
    Spades
}
console.log(Suits.Diamonds === 0) // true

それぞれの列挙子には値を割り当てることができます。
上のSuitsには値を割り当てていませんが、そのような場合TypeScriptは0から数値を割り当てます。

数値型enumの注意点

「TypeScriptのEnumは使わない方が良い」という意見を聞いたことがある方もいらっしゃるかと思いますが、
1番の問題としては数値型のenumにはすべての数値を割り当てることができてしまう点かと思います。

const suit: Suits = 10000; // OK

これを回避するためには文字列型のenumを使用するか、もしくはUnion型を使用する方法があります。

enum Suits {
    Diamonds = "Diamonds",
    Clubs = "Clubs",
    Hearts = "Hearts",
    Spades = "Spades"
}
const suit: Suits = Suits.Diamonds;

type Suits = "Diamonds" | "Clubs" | "Hearts" | "Spades";
const suit: Suits = "Diamonds";

class

ES2015から導入されたclass宣言はもちろんTypeScriptでも利用できます。

class Animal {
    // TypeScriptではprivate / protected / publicが使用できる
    private species?: string;
    constructor(species: string) {
        this.species = species;
    }
}
const animal = new Animal('dog');
animal.species = 'cat'; 
// Property 'species' is private and only accessible within class 'Animal'.

speciesフィールドはprivateなため、クラス外部から触ることはできずエラーになっています。

Union型

TypeScriptの大きな特徴としてよく挙げられるのがこのUnion型です。型同士を|でつなぐことで作成できます。
Unionの名前通り、Union型は構成する型がとる値それぞれの和集合になります。

type StrOrNum = string | number;
let val: StrOrNum = "str";
val = 123;
val = true; // Type 'boolean' is not assignable to type 'string | number'.

この例のStrOrNumはstringがとりうる値とnumber型がとりうる値の両方をとります。

Intersection型

Intersection型はそれぞれの型いずれにも割り当てられる型を作り出します。 Union型を和集合とみるなら、Intersection型は積集合を作り出すイメージです。

type StrOrNum = string | number;
type BoolOrNum = boolean | number;
type I = StrOrNum & BoolOrNum;
let val: I = 123; // valはnumber型になる
val = 'str'; // Type 'string' is not assignable to type 'number'.
val = true; // Type 'boolean' is not assignable to type 'number'.

その他の型

Function型

Function型は全ての関数を受け付ける型です。
引数の型や数、戻り値の型などは指定できないため、object型と同じく使用頻度は高くありません。

let fun: Function = (arg: string) => console.log(`Hello, ${arg}!!`);
fun('TypeScript'); // "Hello, TypeScript!!"
fun() // "Hello, undefined!!" 

let fun2: (arg: string) => void = (arg: string) => console.log(`Hello, ${arg}!!`);
fun2(); // Expected 1 arguments, but got 0.

funargという引数が必要ですが、Function型で宣言しているためにTypeScriptは警告を出しません。
基本的には上の例で使用した(arg: type) => returnTypeという形式でアノテーションします。

Index Signitures

キーの型、値の型を指定してプロパティの個数は指定しないようなオブジェクトも定義できます。

type T = {
    [key: number]: string
}
const t: T = {
    1: 'one',
    2: 'two',
    3: 'three'
};
// このコードはエラーにならないので注意
console.log(t[0].length); // Cannot read property 'length' of undefined

特殊な型

any

anyは何でもありの型です。どのような型でも受け付け、どのような型にも割り当てられます。

let any: any;
any = 123;
any = { a: 123, b: 'string' };
any = null;
any = 123 as unknown;
const s: undefined = any;

any型は実際の値がどのような型か全くわからないため、TypeScriptのメリットを享受しづらくなってしまいます。
プロジェクトをJavaScriptからTypeScriptに置き換える際に、とりあえずanyとして型をつけておくのは便利ですが、
できるなら使用は避けるべきです。
型が実行時までわからないような場合には、unknown型の使用をまずは検討しましょう。

unknown

TypeScript 3.0から導入されました。 unknown型の変数はどのような値も代入可能な点でanyと同様ですが、変数を利用するときには その変数の値の型が実際は何であるかが判明するまで利用することができません。

let unknown: unknown;
unknown = 123;
unknown = { a: 123, b: 'string' };
// 比較は可能
if (!!unknown) {
    console.log('truthy');
}
unknown = 'Hello unknown';
if (typeof unknown === 'string') {
    console.log(unknown.substring(0, 5));
}

上の例ではtypeof演算子を使用し、変数unknownの実際の型が何であるかをチェックしてから String.substringメソッドを使用しています。
型をチェックしなければ変数を使用できないという面でany型の変数よりも安全です。

void

return文で値を返さない関数の戻り値はvoid型として推論されます。

function f() {}
type R = typeof f; // () => void

// undefinedはvoid型に代入可能
let r = f();
r = undefined;

never

never型はとりうる値が一つもない型です。
never型の変数にはanyすら代入できません。

let never: never;
never = {}; // Type '{}' is not assignable to type 'never'.
never = 123 as any; // Type 'any' is not assignable to type 'never'.

とりうる値が一つもないため、never型とその他の型のUnion型においてnever型は無視されます。

type T = string | number | never; // Tは string | number 型

使い所としては、実行したら2度と呼び出し元の関数に処理が戻らないような関数の戻り値があります。
以下はNodeのProcess.exit関数の型です。

NodeJS.Process.exit(code?: number | undefined): never

exitを呼び出した時点で指定されたステータスコードでプロセスを終了するため、呼び出し元に処理が戻ることはありません。

その他、後述するConditional Typesにおいても使用されています。

インターフェースと型エイリアス

インターフェース

TypeScriptにはインターフェースという仕組みがあり、オブジェクトの構造を指定することができます。

interface User {
    name: string,
    id: number,
    registeredDate: Date
}

const user: User = {
    name: 'Guest',
    id: 0,
    registeredDate: new Date('1900-01-01')
}

Classに実装することもできます。

class UserClass implements User {
    constructor(
        public name: string,
        public id: number,
        public registeredDate: Date
    ) {
    }
}

エイリアスもしくは他のインターフェースを継承できます。

type Animal = { name: string, age: number }

interface Cat extends Animal {
    mew(): void
}

const tama: Cat = { name: 'tama', age: 2, mew() { console.log("mew") } }

2つの違い

TypeScriptには型エイリアスとインターフェースが存在し、共にオブジェクトの構造を定義することができます。
この2つは何が違うのでしょうか?

宣言のマージ

インターフェースは同名のインターフェースを重複して宣言できます。 その場合はインターフェースの定義をTypeScriptが自動的にマージします。

interface User {
    id: number
}

interface User {
    // 同一のプロパティは同じ型でなければならない
    // id: string,
    name: string,
}

const user: User = { id: 0, name: "Guest" };

外部ライブラリの型を拡張したい場合インターフェースで宣言されていれば、
同名のインターフェースを再度宣言するだけで拡張が可能です。

対して、同名の型エイリアスはエラーになります。

// Duplicate identifier 'User'.
type User = { 
    id: number
}
type User = {
    name: string
}

プリミティブ型とUnion型

インターフェースでは型エイリアスで表現できる以下のような型を表現できません。

// プリミティブ型
type ID = number;
// Union型
type NumOrStr = number | string;

その他

インデックスシグネチャを型のプロパティとして持つ際に違いがあるようです。
assignabiltity between interfaces and types · Issue #14736 · microsoft/TypeScript · GitHub

ジェネリクス

ジェネリクスはTypeScriptに限らず様々な言語で導入されている、型を抽象化するための仕組みです。
配列のそれぞれの要素に関数を適用するmap関数はジェネリクスを使用して以下のように定義できます。

// <T>によって型変数Tを導入できる
function map<T>(arr: T[], f: (element: T) => T): T[] {
    const result = [];
    for (const e of arr) {
        result.push(f(e))
    }
    return result;
}

console.log(map([1, 2, 3], elm => elm * 2))

ジェネリクスを導入できる場所

関数型

上のmap型のような書き方も可能ですが、アロー関数でももちろんジェネリクスを使用できます。

const map = <T>(arr: T[], f: (e: T) => T): T[] => { /* ... */ }

エイリアス

type F<T, R> = (arg: T) => R;
const f: F<number, string> = arg => arg.toString();

インターフェース

interface F<T, R> {
    (arg: T): R;
}
const f: F<number, string> = arg => arg.toString();

tsconfig.json

tsconfig.jsonはTypeScriptコンパイラの挙動を指定するための設定ファイルです。
以下のコマンドを実行するとカレントディレクトリ上にデフォルトのtsconfig.jsonが作成されます。

$ tsc --init

tsconfigで設定可能なオプションの一覧は以下で確認できます。 TypeScript: TSConfig Reference - Docs on every TSConfig option

型を操作する

型のキーワード

ここではTypeScriptの型にまつわるキーワードについて紹介します。

typeof

typeof valでvalの値の型を取得できます。

const n = 123;
const s = 'str';
const o = { n, s };
const u = undefined;
// T1 = 123
type T1 = typeof n;
// T2 = "str"
type T2 = typeof s;
// T3 = {s: string, n: number}
type T3 = typeof o;
// T4 = undefined
type T4 = typeof u;

keyof

keyofkeyof Typeとすることで、Typeが持つプロパティをUnion型として得ることができる演算子です。

type User = {
    id: number;
    name: string;
    age: number;
}

// type UserKey = "id" | "name" | "age"
type UserKey = keyof User; 

Lookup Types

Lookup Typesは特別なキーワードがあるわけではありませんが、keyofと組み合わせて使われることが 多いためここで紹介します。
ある型TのプロパティKの型をT[K]として参照できる機能です。

type User = {
    id: number;
    name: string;
    age: number;
}

// type UserIdType = number
type UserIdType = User['id'];

in

for..in構文でも使用されるinですが、TypeScriptではMapped Typesと呼ばれる構文のためのキーワードとしても用いられます。

// type U = {A: any, B: any, C: any}
type U = {
    [K in "A" | "B" | "C"]: any
}

inの後ろにある要素(Union型の場合には1つずつ取り出すイメージ)をキーとしたプロパティを作成します。

extends

classやinterface,typeなどを継承する際に用いられるextendsキーワードですが、Conditional Typesと呼ばれる機能でも使用されています。

// T1 = true
type T1 = number extends number | string ? true : false;

// T2 = false
type T2 = number extends null ? true : false;

T extends U ? X : Yとなっている箇所がConditional Typesです。TypeScript 2.8から導入されました。
TがUに割り当て可能であればX、割り当て不可であればYを返します。

上の例ではnumber型はnumber | string型にもちろん割り当てられますので、T1はtrue型になります。
しかしnull型には割り当てられませんので、T2はfalse型になります。

infer

inferは推論した型を型変数として使用するための仕組みです。Conditional Types内で使用できます。 例としてこの後紹介するUtility TypesからReturnTypeという型を見てみます。

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

ジェネリクスT extends (...args: any) => anyの部分はTに割り当てられる型を関数型に制限しています。
右辺のT extends (...args: any) => infer RはTが関数型かどうかをextendsで判定するとともに、
関数の戻り値の型をRという新たな型変数にバインドしています。
当然Tが関数型でない場合にはRに値がバインドされないので、Rが使用できるのはTが関数型の場合のみです。
その他の場合にはany型になります。

Utility Types

最後にTypeScript組み込みの、いくつかの便利な型について紹介します。
上のkeyofやextendsなどの使用例が盛り沢山です。

Partial / Required / Readonly

この3つは似たような定義なのでまとめて紹介します。

type Partial<T> = {
    [P in keyof T]?: T[P];
};

type Required<T> = {
    [P in keyof T]-?: T[P];
};

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

Partialはオブジェクトのプロパティを省略可能(key?: valueの構文)にします。
Mapped Typeを用いてTのプロパティを列挙して、プロパティに?をつけることで全てのプロパティを省略可能にしています。
T[P]の箇所はLookup Typesで、各プロパティの値の型を参照しています。

Requiredは全てのプロパティを省略不可に、Readonlyは変更不可にする型です。

補足: readonlyについて
readonlyはプロパティを変更不可にするキーワードです。

type U = {
    a: string,
    b: number
}
type T = Readonly<U>
const t: T = {
    a: "str",
    b: 123,
}
t.a = "modified"; // Attempt to assign to const or readonly variable

readonly?と同様に-readonlyとすることで、各プロパティからreadonlyを取り除くことができます。

Pick<T, K>

Pickはオブジェクトの型から指定したプロパティのみを持つオブジェクトを作る型です。

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
// T = { a: string, b: number }
type T = Pick<{ a: string, b: number, c: boolean }, "a" | "b">

K extends keyof Tの部分は、Kが指定できるプロパティをTが持つプロパティのみに制限しています。

Record<K, T>

Recordは2つの型K, Tを受け取り、Kの各プロパティに対してTを値としたオブジェクトの型を作る型です。

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

keyof anyは確認してみるとわかりますが、string | number | symbolというUnion型です。

type K = string | number | symbol

つまりK extends keyof anyはキーとする型Tを、キーとして指定できる型に制約しているということになります。

Exclude<T, ExcludedUnion>

Excludeは型TからExcludedUnionの各要素を除いたものを作る型です。

type Exclude<T, U> = T extends U ? never : T;

// type E = 123 | "str"
type E = Exclude<123 | "str" | (() => any), Function>

123 | "str" | (() => any)というUnion型から、Function型に割り当て可能な型のみを取り除いています。
never型を返却する理由は、先の方でも述べましたが、以下のようにUnion型におけるneverはないものとして扱えるためです。

// U1 = string | number
type U1 = string | number | never;
// U2 = void
type U2 = null | void | never;

Union Distribution

上の例のように、T extends UのT が型変数かつUnion型の場合、TypeScriptはUnion Distributionという特殊な挙動になります。

type Exclude<T, U> = T extends U ? never : T;
// type E = 123 | "str"
type E1 = Exclude<123 | "str" | (() => any), Function>

// E1はこんな感じに分配されるイメージ
type E2 = 123 extends Function ? never : 123 
         | "str" extends Function ? never : "str" 
         | (() => any) extends Function ? never : (() => any);

参考: TypeScript: Documentation - Advanced Types#Distributive conditional types

Extract<T, Union>

Excludeの逆です。

type Extract<T, Union> = T extends Union ? T : never;

Omit<Type, Keys>

TypeScript 3.5から導入されました。
Pickとは対照的に、プロパティをKeysとしてUnion型で指定しTypeから取り除きます。

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// T = { c: boolean }
type T = Omit<{ a: string, b: number, c: boolean }, "a" | "b">

既出のUtility TypesであるPickとExcludeを使用しています。
ExcludeでTのプロパティからKを除き、残ったプロパティのみの型をPickで作成しています。

NonNullable

型Tを受け取り、名前通りnullはもちろん、undefinedもTから取り除きます。

type NonNullable<T> = T extends null | undefined ? never : T;

// U = string | (() => any)
type U = NonNullable<string | null | undefined | (() => any)>

Parameters

関数型を受け取り、引数の型をタプル型として返却します。

type Parameters<T extends (...args: any) => any> = 
                T extends (...args: infer Arg) => any 
                ? Arg : never;

// P1 = [a: number, b: string]
type P1 = Parameters<(a: number, b: string) => void>

// P2 = type P2 = [a: { b: number, c: boolean }]
type P2 = Parameters<(a: {b: number, c: boolean}) => string>

// P3 = unknown[]
type P3 = Parameters<any>

inferを使用して引数の型を推論し、結果をArgという新たな型変数にバインドしています。

ConstructorParameters

Parameterのコンストラクタ版です。

type ConstructorParameters<T extends new (...args: any) => any> = 
                T extends new (...args: infer Arg) => any ? Arg : never;

class A {
    constructor(
        private id: number,
        private name: string,
    ) {}
}
// U = [id: number, name: string]
type U = ConstructorParameters<typeof A>

T extends new (...args: any) => anyでTの取りうる型をコンストラクタに制限しています。

ReturnType

Parametersの戻り値版です。

type ReturnType<T extends (...args: any) => any> 
        = T extends (...args: any) => infer R ? R : any;

// U1 = void
type U1 = ReturnType<() => void>

function f() { return Math.random() < 0.5 ? "OK" : "NG" }
// U2 = "OK" | "NG"
type U2 = ReturnType<typeof f>

inferで今度は戻り値の型を推論しています。

InstanceType

constructorの戻り値の型を取得します。 ReturnTypeのコンストラクタ版です。

type InstanceType<T extends new (...args: any) => any>
        = T extends new (...args: any) => infer Class ? Class : any;

class C {
    constructor(
        private name: string = "Guest",
        private id: number = 0
    ) {}
}
// U1 = C
type U1 = InstanceType<typeof C>
// U2 = any
type U2 = InstanceType<any>

ThisParameterType

Tが関数型でthisをパラメータとして持つ場合にその型を返却します。

type ThisParameterType<T> 
        = T extends (this: infer This, ...args: any) => any ? This : unknown;

function f1(this: number) {}
function f2(arg: number) {}

// F1 = number
type F1 = ThisParameterType<typeof f1>
// F2 = unknown
type F2 = ThisParameterType<typeof f2>

thisパラメータは必ず引数の先頭である必要があるため、それを利用してthisの型を推論しています。

OmitThisParameter

関数型Tを受け取り、Tがthisパラメータを含んでいればそれを除いた関数型を返します。

type OmitThisParameter<T> 
        = unknown extends ThisParameterType<T> 
        ? T 
        : T extends (...args: infer A) => infer R 
        ? (...args: A) => R 
        : T;

// F1 = (a: string) => any
type F1 = OmitThisParameter<(this: number, a: string) => any>

// F2 = 123
type F2 = OmitThisParameter<123>

unknown extends ThisParameterType<T>でTがthisパラメータを含むかどうか確認しています。
thisは可変長引数のパラメータに含まれないようなので、関数型であれば可変長引数の型を推論して返します。

ThisType

この型を利用するためには--noImplicitThisを有効にする必要があります。
公式の例を引用して説明します。
TypeScript: Documentation - Utility Types#ThisType

type ObjectDescriptor<D, M> = {
    data?: D;
    methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
    let data: object = desc.data || {};
    let methods: object = desc.methods || {};
    return { ...data, ...methods } as D & M;
}

let obj = makeObject({
    data: { x: 0, y: 0 },
    methods: {
        moveBy(dx: number, dy: number) {
            this.x += dx; // Strongly typed this
            this.y += dy; // Strongly typed this
        },
    },
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

ObjectDescriptor内のmethods?: M & ThisType<D & M>は、methods内で使用されるthisの型をobjの型であるD & M型にアノテーションします。
D & M型はdataの型とmethodsの型の交差型なので、今回は以下のような型になります。

{
    x: number;
    y: number;
    moveBy(dx: number, dy: number): void;
}

makeObjectの引数がObjectDescriptor型なので、method内のthisが上のような型に推論されるためthis.xの形式でxにアクセスできる、ということだと思われます。

ちなみにThisTypeでアノテーションしない場合には関数内のthisは以下の型になりますので、TypeScriptはエラーを出します。

moveBy(dx: number, dy: number) {
    // Property 'x' does not exist on type '{ moveBy(dx: number, dy: number): void; }'
    this.x += dx;
    // Property 'y' does not exist on type '{ moveBy(dx: number, dy: number): void; }'
    this.y += dy;
}

Uppercase / Lowercase

ここから紹介する4つの型はTypeScript 4.1から導入された新しめの型です。せっかくですので紹介します。
いずれも文字列型を操作できます。

// U1 = "FOO"
type U1 = Uppercase<"foo">;
// U2 = "bar"
type U2 = Lowercase<"BAR">

Capitalize / Uncapitalize

// C1 = "Foo"
type C1 = Capitalize<"foo">;
// C2 = "bar"
type C2 = Uncapitalize<"Bar">

これらは単体で使うというよりかは、同じく4.1から導入された仕組みであるTemplate literal typesの補助として使われるのではと思います。
Template literal typesの説明は流石に入門記事の範囲外のため割愛します。詳しく知りたい方はこちらをご覧ください。
TypeScript: Documentation - Template Literal Types

まとめ

入門記事ということで長めになってしまいましたが、これをきっかけに少しでもTypeScriptに興味を持っていただけたなら嬉しい限りです。
紹介していないTypeScriptの機能もまだまだあるようですので、また機会があれば書かせていただきたいと思います。

参考


  • エンジニア中途採用サイト
    ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
    ご興味ありましたら是非ご確認をお願いします。
    20210916153018
    https://career-recruit.rakus.co.jp/career_engineer/

  • カジュアル面談お申込みフォーム
    どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
    以下フォームよりお申込みください。
    forms.gle

  • イベント情報
    会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! rakus.connpass.com

Copyright © RAKUS Co., Ltd. All rights reserved.