TypeScript

https://legacy.gitbook.com/book/basarat/typescript

tsconfig.json

paths

import のパス解決は大まかにわけて 2 通りある。

  • 相対パス指定(import Hoge from "./hoge"): 相対パスで解決
  • それ以外(import Hoge from "hoge"): node_modules/hoge/index.d.tsで解決

このうち、相対パス指定以外の場合はパスの解決をtsconfig.jsonで上書きできる。 これは、以下の場合などに有効。

  • @types/***が存在しないライブラリを利用するときに、自分で用意した型定義を読みたいとき
  • 相対指定だとパスが長くなる(例: "../../hoge/fuga")のでシンプル(例: @component/fuga)に書きたいとき

パスの指定は、tsconfig.jsonbaseUrlpathsを設定するとできる。

{
  "baseUrl": "src",
  "paths": {
    "@app/*": [
      "app/*"
    ] /* import hoge from "@app/hoge" と書くと、"src/app/hoge" を見に行くようになる */,
    "@config/*": ["app/_config/*"],
    "@environment/*": ["environments/*"],
    "@shared/*": ["app/_shared/*"],
    "@helpers/*": ["helpers/*"]
  }
}

文法

Type Guard

戻り値を[引数の名前] is [型名]とすると、型チェック済みをコンパイラに知らせることができる。

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

// 戻り値が`true`の場合は`some`を`User`型とコンパイラが認識する
const isUser = (some: any): some is User => true;

const hoge = 1;
if (isUser(hoge)) {
  // コンパイルエラーにならない
  console.log(hoge.name);
}

ジェネリックと組み合わせると汎用的な安全キャストが実装できる。

import Joi from "joi";
const safeCast = <T>(value: any, schema: Joi.ObjectSchema): value is T => {
  const error = schema.validate(value, { abortEarly: false }).error;
  if (error) {
    console.warn(error);
    return false;
  }
  return true;
};



// 使い方
const some = 1;
if (safeCast<Hoge>(some, schema)) {
  // ここでsomeはHoge型
  console.log(some);
}

Conditional Type

関数の引数

3.0 から使える。

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

Predefined conditional types: 定義済みConditional Types

  • Exclude<T, U>: T割り当て可能なタイプから除外するU。
  • Extract<T, U>: にT割り当て可能なタイプから抽出するU。
  • NonNullable<T>: 除外nullしてundefinedからT。
  • ReturnType<T>: 関数型の戻り型を取得してください。
  • InstanceType<T>: コンストラクタ関数型のインスタンス型を取得します。

Ref: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html https://qiita.com/nahokomatsui/items/856c0cb07a380305521a

readonlyを付けるとあとで値が変わらないことが保証されるので、定数型に推論される。

const readonly foo = "Add" // fooの型は"Add"
const bar = "Add" // barの型はstring

Mapped Type

Mapped Type は既存の型のメンバに一括で型変換をしたいときに使う。


既存の型にある特定のプロパティを除いた型を作る

PickExcludeを組み合わせるとできる。

// 既存の型から特定のプロパティを除く型
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

// a, b, cの3つをもつ型
type Hoge = {
  a: number,
  b: number,
  c: number
}

// b, cの2つをもつ型になる
const x: Omit<Hoge, 'a'> = {
  b: 1,
  c: 2
}

Ref: Exclude property from type

既存の型にある特定のプロパティの型を変換する

PickExclude&を組み合わせるとできる。 変換したいプロパティを一度除外してから、好きな型を&で合成する。

// 既存の型から特定のプロパティを除く型
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

type Hoge = {a: number, b: number, c: number}
const hoge = {a: 1, b: 1, c: 1}

// Fugaは、Hogeのaを任意のstring型に変換した型
type Fuga = Omit<Hoge, 'a'> & {a?: string}
const fuga1 = {a: 'test', b: 1, c: 1}
const fuga2 = {b: 1, c: 1}

Ref: Exclude property from type

既存の型のプロパティをすべて任意にした型を作る

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

// a, b, cの3つをもつ型
type Hoge = {
  a: number,
  b: number,
  c: number
}

// a | undefined, b | undefined, c | undefined をもつ型になる
const x: Partial<Hoge> = { b: 1 }

Ref: https://www.typescriptlang.org/docs/handbook/advanced-types.html

tips

Reactコンポーネント関数のプロパティにコンポーネント関数をもたせる

MyComponentMyComponent.SubComponentのようにコンポーネントをグループ化したい場合に便利。

interface Props {}

interface MyComponent extends React.FunctionComponent<Props> {
  SubComponent: React.FunctionComponent<Props>
}

const MyComponent = (props => {
  return <div>This is my component.</div>
}) as MyComponent

export default MyComponent

再代入不可の型を定義する

interfaceのメンバ定義でreadonlyを先頭につける。

interface Point {
    readonly x: number;
    readonly y: number;
}

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

配列の場合はReadonlyArray<T>を使う。

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

Ref: Interfaces - Readonly properties | TypeScript

const vs readonly

次のように使い分ける。

  • const - 変数に使う
  • readonly - プロパティに使う

インタフェースで関数型を表す

関数の引数と戻り値の型のみを書いたinterfaceを定義する。

interface Func {
  (foo: string, bar: number): boolean
}

関数にプロパティをもたせる

interface FuncWithProps {
  (): boolean,
  foo: string
}

let f = (() => true) as FuncWithProps
f.foo = 'Hello World'

Ref: Interfaces · TypeScript

異なる型を含む配列を返すときは戻り値の型注釈を必ずつけること

型注釈をつけない場合はたとえ型が自明でも複合型の配列型扱いになってしまう。

const mixArray = () => [1, 'a']
// a, bはともに number | string
const [a, b] = mixArray()

この問題は明示的に型注釈を書くことで回避できる。

const mixArray = (): [number, string] => [1, 'a']
// aはnumber, bはstring
const [a, b] = mixArray()

kindプロパティを使って型を判定する

// is.ts
type Cat = { kind: 'cat' }
type Dog = { kind: 'dog' }
type Mouse = { kind: 'mouse' }

const is = (kind: string) => (obj: any) => obj != null $$ obj.kind === kind

const cat =  is('cat')
const dog =  is('dog')
const mouse =  is('mouse')

使う側:

import * as is from './is'

const cat = { kind: 'cat' }

console.log(is.cat(cat))

Ref: https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions

文字列定数を文字列リテラル型として使う

typeof [変数名]でできる。

const HOGE = 'aaa'
const FUGA = 'bbb'

type HogeOrFuga = typeof HOGE | typeof FUGA
// これは以下と同じ
// type HogeOrFuga = 'aaa' | 'bbb'

読み取り専用

const readOnlyArray: ReadOnlyArray<number> = [1, 2, 3];
readOnlyArray.push(1); // コンパイルエラー

type vs interface

type がおすすめされていた。 Interface vs Type alias in TypeScript 2.7

任意の型パラメータ

= {}を書けばいい。

const func = <T, U = {}>(val1: T, val2: U)

// 普通に使う場合
func<number, string>(123, 'hoge')

// 第二パラメータはstringに推論される
func<number>(123, 'hoge')

ただし、型を省略して呼び出すと返却型が{}になってしまうことがある。例えばこのような場合。

const func = <T, U = {}>(val1: T, val2: U): U

// 問題なし
const a: string = func<number, string>(1, 'hoge')

// 問題あり:第二引数に'hoge'を入れているため戻り値をstringと推論してほしいが、デフォルト値`{}`が勝ってしまっている
const b: {} = func<number>(1, 'hoge')

そういう時はExclude<T, U>を使い、{}を除外する。

const func = <T, U = {}>(val1: T, val2: U): Exclude<U, {}>

ref: Typescript optional generic type

既存のプロパティを一括でnullableにする

Partical<T>を使う。

interface Hoge {
  a: number,
  b: string,
  c: boolean
}

const hoge: Partical<Hoge> = {b: 'aaa'}

アロー関数でオーバーロード

関数の型注釈を、すべてのオーバーロード関数をもつオブジェクトとして定義するとできる。

const handleEvent: {
  (event: HogeEvent): void;
  (event: FugaEvent): void;
  (event: PiyoEvent): void;
} = (event: HogeEvent | FugaEvent | PiyoEvent) => {
  console.log(event);
};

ref: Can I use TypeScript overloads when using fat arrow syntax for class methods?

型定義ファイル

d.tsファイルを好きな場所に置くと、特にimportなどしなくても自動で型定義を読み込んでくれる。

https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html

グローバル変数

declare var foo: number;
declare let foo: number;
declare const foo: number;

グローバル関数

declare function greet(greeting: string): void;

プロパティをもつオブジェクト

declare namespace myLib {
  function makeGreeting(s: string): string;
  let numberOfGreetings: number;
}

関数

export function myFunc<T>(a: number): T;

使う側

let result = myLib.makeGreeting("hello, world");
console.log("The computed greeting is:" + result);

let count = myLib.numberOfGreetings;

オーバーロード関数

宣言

declare function getWidget(n: number): Widget;
declare function getWidget(s: string): Widget[];

コード

let x: Widget = getWidget(43);

let arr: Widget[] = getWidget("all of them");