Motomichi Works Blog

モトミチワークスブログです。その日学習したことについて書いている日記みたいなものです。

TypeScript学習日記 その0012 とてもやさしいMapped Types入門

参考にさせていただいたページ

とても参考になる入門と初級の記事です。

はじめに

以下のページを読んで、Mapped Typesが理解できる人はこの記事を読む必要は全くありません。

これらのページを読んでもMapped Typesを全く理解できないという人が、Mapped Typesを理解していく為のとっかかりをつかむ。
この記事は、そんな「超入門」記事になっています。

Mapped Typesを使うと、stringの部分型、numberの部分型、symbolの部分型、をキーとしたオブジェクトの型を定義することができます。

部分型についての前提知識

例えば 'foo' | 'bar' のようなstring literal型は、string型の部分型です。

Mapped Typesの最も簡単な例

以下のようにMyObj型を定義した場合、

    type MyObj = {[Key in 'foo' | 'bar']: number};

MyObj型は、以下のように定義したのと同じ型になります。

    type MyObj = {
        foo: number;
        bar: number;
    }

'foo' | 'bar' が順番に変数Keyに入り、MyObj型のキーになっていることがわかります。

'foo' | 'bar'の部分について

さきほどの「Mapped Typesの最も簡単な例」の in の右側の 'foo' | 'bar' の部分には、string | number | symbolの部分型が使用できます。

今回の例でいうと、stringの部分型である 'foo' | 'bar' を使用しています。

当然ですがbooleanなどは使用できません。

Genericsを使ってMyObj型を定義する

Genericsを使って、型を返す関数みたいなものが作れます。

以下のように書いても、MyObj型は「Mapped Typesの最も簡単な例」で定義したものと同じMyObj型になることがわかると思います。

    // Genericsを使ってMyMappedType型を定義(型を返す関数みたいなものを定義)
    type MyMappedType<T extends string | number | symbol> = { [Key in T]: number }

    // MyMappedType<T>を実行して、MyObj型を定義
    type MyObj = MyMappedType<'foo' | 'bar'>

さきほど説明したように、[Key in T]のTにはstring | number | symbolの部分型しか使用できないため、以下のようにMyMappedType型を定義する時点で型引数Tは string | number | symbol の部分型であることを明示しています。

MyMappedType<T extends string | number | symbol>

MyMappedTypeの型引数Tとして、string literal型を渡すことが決まっているのであれば以下のようにstringの部分型であることを明示するのが良いと思います。

MyMappedType<T extends string>

FooBar型を定義する

以下のようにFooBar型を定義しても同じMyObj型が定義できることがわかると思います。

    // FooBar型を定義
    type FooBar = 'foo' | 'bar'

    // Genericsを使ってMyMappedType型を定義(型を返す関数みたいなものを定義)
    type MyMappedType<T extends string | number | symbol> = { [Key in T]: number }

    // MyMappedType<T>を実行して、MyObj型を定義
    type MyObj = MyMappedType<FooBar>

おわりに

今回、以下のようにMyObj型のvalueの型はnumberで固定していますが、

{ [Key in T]: number }

以下のように変数Keyをオブジェクトのvalueとして使うこともできます。

{ [Key in T]: Key }

そうするとMyObj型は以下のようになります。

type MyObj = {
    foo: "foo";
    bar: "bar";
}

超入門記事なのでここまでにしますが、以下の記事を読むと、より発展的な使い方やunion distributionなどについて学ぶことができます。

少しでも誰かの理解の助けになれば幸いです。

TypeScript学習日記 その0011 とてもやさしいinfer入門

参考にさせていただいたページ

この記事について

inferまじで何もわからない。という私のような人が少しとっかかりをつかむ超入門記事です。

inferがわからない人の助けになれば幸いです。

でも何に使うと効果的なのかはこの記事を読んでもわかりません。

実例について

参考にさせていただいたページにはinferが使用されている実例として、ReturnTypeという組み込みのジェネリクスが挙げられています。

以下のようなコードだそうです。

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

これはTの部分に関数の型を渡すと、その関数の「返り値の型」を返すというものです。

「返り値の型」が取得できるのでReturnTypeなのでしょう。

ReturnTypeを使ってみる

まず自分で関数の型 MyFunction を定義してみます。

type MyFunction = () => 'hoge'

ReturnTypeを使ってみます。

type Hoge = ReturnType<MyFunction> // type Hoge = "hoge"

Hogeはstring literal型の "hoge" になりました。

ReturnTypeを読む

さきほどの「ReturnTypeを使ってみる」例を踏まえて、以下のコードを読んでみましょう。

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

(...args: any[]) の部分を読むと、引数は可変長引数でany型なので「引数はあっても無くてもよく、引数の型は何でも良い」ということが読み取れます。

また infer R の部分は返り値部分を推測する。つまり「返り値は何でも良い」ということが読み取れます。

つまり渡した型Tが関数の型であれば、Rが返ってきますので、さきほどの例ではstring literal typeの"hoge"が返ってきます。

もう少し考えてみる

inferの理解を深めるためにもう少し考えてみます。

以下のようにしたときtype Elem = "elm1" | "elm2"になります。

    type MyArr = ('elm1' | 'elm2')[]
    type ElemTypes<T> = T extends (infer E)[] ? E : any
    type Elem = ElemTypes<MyArr>

type MyArrは文字列'elm1'または'elm2'が入る配列の型です。

T extends (infer E)[]は渡されたTが配列の型だった場合にEを返します。

つまり、type Elem = "elm1" | "elm2"になります。

このような例を書いてしまいましたが、実際はこんなElemTypes<T>を定義しなくても、配列の要素の型が欲しい場合は以下のようにすれば同じ型が得られます。

type Elem = MyArr[number]

TypeScript学習日記 その0010 便利な型Tipsメモ

はじめに

ほんとに便利な型Tipsを公開してくださっている記事がいくつかあり、あとで見つからなくなったらつらいのでメモしておきます。

リンク

TypeScript学習日記 その0009 readonlyな文字列の配列をキーとしてオブジェクトを作成する関数

参考にさせていただいたページ

makeObj.tsに関数を定義する

以下のような関数を定義します。

type MyObj<K extends string, T> = { [K0 in K]: T }
type MyKeys<T extends string> = readonly T[]

export default function makeObj<K extends string, T>(
  myKeys: MyKeys<K>,
  defaultValue: T
): MyObj<K, T> {
  const myObj = {} as MyObj<K, T>
  for (const key of myKeys) {
    myObj[key] = defaultValue
  }

  return myObj
}

任意のファイルにimportして実行する

以下のようにimportして関数を実行すると任意のプリミティブ型の初期値を持ったオブジェクトが作成できます。

import makeObj from '~/utils/makeObj'

const myKeys = [
  'key1',
  'key2',
  'key3',
] as const

const myStrObj = makeObj(myKeys, '')
const myBooleanObj = makeObj(myKeys, false)
const myStrArrObj = makeObj(myKeys, [] as string[])
const my0bj = makeObj(myKeys, makeObj(['hoge', 'foo'] as const, ''))

TypeScript学習日記 その0008 引数の型が決まっていない関数を定義する(関数実行時に引数の型が決まる関数を定義する)

参考にさせていただいたページ

はじめに

関数を定義する段階では引数の型は決めずに、実行時に渡す引数の型によって決まってほしいときがあると思います。

そのようなときに便利な方法です。

関数の定義の仕方

ジェネリクスを使用して、以下のように関数を定義すると引数の型が決まっていない関数を定義することができます。

function myFunction<T>(arg: T) {
  return arg
}

関数を実行する

あとは以下のように実行します。

const myStr = myFunction('')
const myNum = myFunction(10)

実用的な例

以下のような関数を定義してみました。

TypeScript学習日記 その0009 readonlyな文字列の配列をキーとしてオブジェクトを作成する関数 - Motomichi Works Blog

TypeScript学習日記 その0007 構造的部分型について学習する

参考にさせていただいたページ

はじめに

きっかけとなったのはこちら「TypeScriptの型入門 - Qiita」の入門記事であり、キーが多い方が少ない方の部分型なの?って思って検索した次第です。
TypeScript の構造的部分型とプリミティブ型について - 30歳からのプログラミング」を読むことでとても理解が捗りました。

コードを書いてみる

以下のようなコードで、FooBarObj型はFooObj型の部分型と言える。

    type FooBarObj = {
      foo: string
      bar: number
    }
    type FooObj = {
      foo: string
    }

    // FooBarObj型の変数を定義する
    const fooBarObj: FooBarObj = {
      foo: 'foo',
      bar: 10,
    }

    // よりkeyが少ないFoo型に代入可能
    const fooObj: FooObj = fooBarObj

    // console.log(fooObj)で出力してみると { foo: 'foo', bar: 10 } という感じでキーは残っている
    console.log(fooObj)

    // FooObj型なので、console.log(fooObj.foo)はOK
    console.log(fooObj.foo)

    // FooObj型なので、console.log(fooObj.bar)はエラーになる
    console.log(fooObj.bar)

    // FooObj型の変数を、よりkeyが多いFooBarObj型に代入しなおすことはできないのでエラーになる
    const fooBarObj2: FooBarObj = fooObj
    console.log(fooBarObj2.bar)

TypeScript学習日記 その0006 オブジェクトの型を複数定義して|で合わせたり&で合わせたりしてみる(union型とintersection型)

オブジェクトの型を定義する

以下のように定義しました。

    type MyObjA = { a1: string; a2: string }
    type MyObjB = { b1: string; b2: string }
    type MyObjC = { c1: string; c2: string }
    type MyObjUnion = MyObjA | MyObjB | MyObjC
    type MyObjAnd = MyObjA & MyObjB & MyObjC

定義したMyObjUnion型を使ってみる

以下のようにすると、エラーになりました。

    const myObj1: MyObjUnion = {
      a1: 'str',
      b1: 'str',
      c1: 'str',
    }

以下のようにすると、OKになりました。

    const myObj1: MyObjUnion = {
      a1: 'str',
      a2: 'str',
      b1: 'str',
      c1: 'str',
    }

MyObjUnion型を構成する3つの型のうち1つ以上を満たすと、残る2つの型に含まれるキーはあっても無くても良いみたいです。

定義したMyObjAnd型を使ってみる

以下のようにすると、エラーになりました。

    const myObj2: MyObjAnd = {
      a1: 'str',
      a2: 'str',
      b1: 'str',
      c1: 'str',
    }

MyObjAnd型を構成する3つの型に含まれるキー全てを満たす必要があるみたいです。

これについて調べてみる

以下のような記事が出てきました。

union型(合併型)とintersection型(交差型)についても調べてみると良いかもしれません。