Closed61

TypeScriptの型 入門/初級/演習

hajimismhajimism
hajimismhajimism

const と let の型推論の違い知らなかった

JavaScriptにおけるconstは変数に再代入されないことを保証するものですから、aにはずっと'foo'が入っていることが保証され、aの型を'foo'とできます。一方、constではなくletやvarを使って変数を宣言した場合は変数をのちのち書き換えることを意図していると考えられますから、最初に'foo'が入っていたからといって変数の型を'foo'型としてしまっては、他の文字列を代入することができなくて不便になってしまいます。そこで、letやvarで変数が宣言される場合、推論される型はリテラル型ではなく対応するプリミティブ型全体に広げられます

hajimismhajimism

構造的部分型という語彙を得た

一方、TypeScriptでは構造的部分型を採用しているため、次のようなことが可能です。

interface MyObj {
  foo: string;
  bar: number;
}

interface MyObj2 {
  foo: string;
}

const a: MyObj = {foo: 'foo', bar: 3};
const b: MyObj2 = a;

MyObj2というのはfooプロパティだけを持つオブジェクトの型ですが、MyObj2型変数にMyObj型の値aを代入することができています。MyObj型の値はstring型のプロパティfooを持っているためMyObj2型の値の要件を満たしていると考えられるからです。ちなみに、一般にこのような場合MyObjはMyObj2の部分型であると言います。

hajimismhajimism

こんな意図ありげな実装があったなんて知らなかった。

変数bに代入しようとしている{foo: 'foo', bar: 3}はfooプロパティがstring型を持つため、先ほどの説明からすれば、barプロパティが余計であるもののMyObj2型の変数に代入できるはずです。しかし、オブジェクトリテラルの場合は余計なプロパティを持つオブジェクトは弾かれてしまうのです。

これは、多くの場合余計なプロパティを持つオブジェクトリテラルを意図的に用いることが少なく、ミスである可能性が高いからでしょう。実際、TypeScriptの型システムを順守している限り、このような余計なプロパティは存在しないものと扱われるためアクセスする手段が無く、無駄です。どうしてもこのような操作をしたい場合はひとつ前の例のように別の変数に入れることになります。一度値を変数に入れるだけで挙動が変わるというのは直感的ではありませんが、TypeScriptでは入口だけ見ていてやるからあとは自己責任でということなのでしょう。

hajimismhajimism

可変長引数使ったこと無いかも

const func = (foo: string, ...bar: number[]) => bar;

func('foo');
func('bar', 1, 2, 3);
// エラー: Argument of type '"hey"' is not assignable to parameter of type 'number'. 
func('baz', 'hey', 2, 3);
hajimismhajimism

たまにアロー関数でジェネリクス使う書き方忘れちゃうのよね

const f: <T>(obj: T)=> void = func;
hajimismhajimism

可変長タプルおもろいな

また、TypeScriptのタプル型は、可変長のタプル型の宣言が可能です。それはもはやタプルなのかという疑問が残りますが、これは実質的には最初のいくつかの要素の型が特別扱いされたような配列の型となります。

type NumAndStrings = [number, ...string[]];

const a1: NumAndStrings = [3, 'foo', 'bar'];
const a2: NumAndStrings = [5];
// エラー: Type 'string' is not assignable to type 'number'.
const a3: NumAndStrings = ['foo', 'bar'];
hajimismhajimism

こんなことできるんか

さらに、オプショナルな要素を持つタプル型もあります。これは、[string, number?]のように型に?が付いた要素を持つタプル型です。この場合、2番目の要素はあってもいいし無くてもいいという意味になります。ある場合はnumber型でなければなりません。

type T = [string, number?];

const t1: T = ['foo'];
const t2: T = ['foo', 3];
hajimismhajimism

実は、最近(TypeScript 3.0)になってタプル型の面白い使い道が追加されました。それは、タプル型を関数の可変長引数の型を表すのに使えるというものです。

TSの柔軟性すごいな

hajimismhajimism

タプル型と可変長引数とジェネリクス

ここらへんから話難しくなる。
理解の確認のために、手元でサンプルコードを再現できるか確かめる。

function bind<T, U extends any[], R>(
  func: (arg: T, ...rest: U) => R,
  value: T
): (args: U) => R {
  return (args) => func(value, ...args)
}
hajimismhajimism

さて、ジェネリクスなど、ここまで説明してきた要素の多くは型のある言語なら普通にあるものだと思います。しかし、ここで紹介するunion型を持っている言語はそこまで多くないのではないかと思います。

そうなんや。DartにUnionなかった気がするけどそれはDartが貧弱だからだと思ってた。

hajimismhajimism

Reactが書きやすいのはTSが頑張ってくれているおかげでもあるなあ

また、&&や||が短絡実行するという挙動を用いたテクニックもJavaScriptではよく使われますが、これもTypeScriptは適切に型検査してくれます。上の関数funcは次のようにも書くことができます。

hajimismhajimism

これまで見たように、プリミティブ型ならunion型の絞り込みはけっこういい感じに動いてくれます。しかし、やはりオブジェクトに対してもいい感じにunion型を使いたいという需要はあります。そのような場合に推奨されているパターンとして、リテラル型とunion型を組み合わせることでいわゆる代数的データ型(タグ付きunion)を再現する方法があります。

代数的データ型(タグ付きunion)という語彙を得た。ReducerのAction typeね。

hajimismhajimism

never型の使いどこよくわかってない。これはなるほどな例。

function func(): never {
  throw new Error('Hi');
}

const result: never = func();

関数の返り値の型がnever型となるのは、関数が値を返す可能性が無いときです。これは返り値が無いことを表すvoid型とは異なり、そもそも関数が正常に終了して値が返ってくるということがあり得ない場合を表します。上の例では、関数funcは必ずthrowします。ということは、関数の実行は中断され、値を返すことなく関数を脱出します。特に、上の例でfuncの返り値を変数resultに代入していますが、実際にはresultに何かが代入されることはあり得ません。ゆえに、resultにはnever型を付けることができるのです。

hajimismhajimism

多くの場合、bar?: number;よりもbar: number | undefinedを優先して使用することをお勧めします。前者はbarが無い場合に本当に無いのか書き忘れなのか区別ができず、ミスの原因になります。後者の場合は書き忘れを防ぐことができます。

Component propsのinterfaceとかは結構?で定義しちゃうなー

hajimismhajimism

オプショナルなプロパティの挙動は、exactOptionalPropertyTypesコンパイラオプションが有効かどうかによって変わります。デフォルトではこのオプションは無効で、比較的最近 (TypeScript 4.4) 追加されたオプションということもあり、無効にしているプロジェクトの方が多いでしょう。

一方で、exactOptionalPropertyTypesが有効の場合、オプショナルなプロパティにundefinedを入れることができなくなります。

親切な記事だなあ

hajimismhajimism

実は、オブジェクト型の記法で関数型を表現する方法があります。

知らんかった

hajimismhajimism

さて、以上の2つと同時に導入されたのがmapped typeと呼ばれる型です。日本語でどう呼べばいいのかはよく分かりません。mapped typeは{[P in K]: T}という構文を持つ型です。ここでPは型変数、KとTは何らかの型です。ただし、Kはstringの部分型である必要があります。例えば、{[P in 'foo' | 'bar']: number}という型が可能です。

「読めるようになりたい」と思ってた文法。in objと似たような感じのinだな

hajimismhajimism

あーだから、任意のオブジェクト型を{ [K in keyof T]: T[K] }で表現できるので、これをなんやかんやすればいろんな型を表現できるよーてことか

hajimismhajimism

逆に、修飾子を取り除くこともTypeScript2.8から可能になりました。そのためには、?やreadonlyの前に-を付けます。例えば、すべてのプロパティから?を取り除く、いわばPartial<T>の逆のはたらきをするRequired<T>は次のように書けます。

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

-?はそういう意味だったのか

hajimismhajimism

自分で書いて理解

function propsStringify<T>(obj: T): { [K in keyof T]: string } {
  const result = {} as { [K in keyof T]: string }

  for (const key in obj) {
    result[key] = String(obj[key])
  }

  return result
}
hajimismhajimism

自分で書いて理解

function pickFirst<T>(obj: { [K in keyof T]: Array<T[K]> }): {
  [K in keyof T]: T[K]
} {
  const result = {} as any

  for (const key in obj) {
    result[key] = obj[key][0]
  }

  return result
}
hajimismhajimism

DeepReadonlyむずい

type DeepReadonly<T> =
    T extends any[] ? DeepReadonlyArray<T[number]> :
    T extends object ? DeepReadonlyObject<T> :
    T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
    readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>;
};

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
hajimismhajimism
type DeepReadonly<T> =
    T extends any[] ? DeepReadonlyArray<T[number]> :
    T extends object ? DeepReadonlyObject<T> :
    T;

Tが配列型→DeepReadonlyArray<T[number]>
Tがobject型→DeepReadonlyObject<T>
Tがプリミティブ型→T

hajimismhajimism

DeepReadonlyArray<T>は、要素の型であるTをDeepReadonly<T>で再帰的に処理し、配列自体の型はReadonlyArray<T>により表現しています。ReadonlyArray<T>というのは標準ライブラリにある型で、各要素がreadonlyになっている配列です。T[number]というのは配列であるTに対してnumber型のプロパティ名でアクセスできるプロパティの型ですから、すなわち配列Tの要素の型ですね。

じっくり読んでもわからん

hajimismhajimism

試す

const a = [1, 2, 3, 4, 5]
type A = DeepReadonly<typeof a>

このときAはDeepReadonlyArray<number>と推論されている。そりゃそうだ。

これがわかんないのかな。

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

DeepReadonlyArray<T>はReadonlyArray<DeepReadonly<T>>の部分型であるのみ。

DeepReadonlyArray<number>はReadonlyArray<DeepReadonly<number>>の部分型
DeepReadonly<number>はそのままnumberなので、これはReadonlyArray<number>、つまり「readonlyなnumberの配列」と同義。

つまりTが配列だとReadonly<T[number]>で返ってくる

hajimismhajimism

もうちょい実験

const a = [1, 2, 3, 4, 5]
const b = [a, a, a]
type B = DeepReadonly<typeof b>

このときBはDeepReadonlyArray<number[]>と推論されている。

type C = B[number]

としてみると、CはDeepReadonlyArray<number>と推論されている。うまく再帰的にReadonly定義されている。

hajimismhajimism

あとはわかるな
再帰が直感的に理解できてない、苦手みたい

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

ReturnType<T>は、Tが関数の型のとき、その返り値の型となります。ポイントは、関数の型の返り値部分にあるinfer Rです。このようにinferキーワードを用いることでconditional typeの条件部分で型変数を導入することができます。導入された型変数は分岐のthen側で利用可能になります。

つまり、このReturnType<T>は、Tが(...args: any[]) => R(の部分型)であるときRに評価されるということです。then側でしか型変数が使えないのは、else側ではTが(... args: any[]) => Rの形をしていないかもしれないことを考えると当然ですね。このことから分かるように、この機能は型に対するパターンマッチと見ることができます。

inferも読めるようになりたかったやつ。infer RのRは新しい変数なのね。Rがあるとして〜っていう表現か。

hajimismhajimism

めっちゃおもろいな、こんなことできるんや

type ExtractHelloedPart<S extends string> = S extends `Hello, ${infer P}!` ? P : unknown;

// type T1 = "world"
type T1 = ExtractHelloedPart<"Hello, world!">; 
// type T2 = unknown
type T2 = ExtractHelloedPart<"Hell, world!">;
hajimismhajimism

ようやく読み終わった
重厚ですごい、改めてありがとうございます

hajimismhajimism
hajimismhajimism

条件型はどちらか片方を返すはずなのに、まさかの両方とは、これは反則もいいところです。この動作を説明するのがunion distributionなのです。

なんとなく使ってたけど、型の計算に分配法則が働いているのね

hajimismhajimism

もう1つ注意しなければいけない点があり、これが条件型の大変ややこしいところでもあります。それは、今まで説明したようなunion distributionが発生するのは条件部分の型が型変数である場合のみであるという点です。

ちょっとむずいな

hajimismhajimism

None / Some / Option型、例示にぴったりの型だな。何か元ネタがあるのかな。

hajimismhajimism
type IsNever<T> = T extends never ? true : false;

// T1はneverになる
type T1 = IsNever<never>;

...
これを避ける方法はついさっき説明したばかりなので省略します。

こうかな?

type IsNever<T> = T[] extends never[] ? true : false
hajimismhajimism
type Foo = { foo: string };
type Bar = { bar: number };

type FooBar = Foo | Bar;

// FooBarArrは{}になる
type FooBarArr = {[P in keyof FooBar]: Array<FooBar[P]>};
// ↓これがエラーにならない!
const val1: FooBarArr = {};

上の例では、keyof FooBarは"foo" & "bar"という型になります。

"foo" | "bar"じゃないんだな...

hajimismhajimism

type FooBar = Foo & Bar とするとkeyof FooBar = "foo" | "bar"となる。この関係面白いな

hajimismhajimism
type Foo = { foo: string; hoge: string }
type Bar = { bar: number; hoge: string }

type FooBar = Foo | Bar

とすると keyof FooBar = 'hoge'になる

hajimismhajimism

具体的には、[P in keyof T]で型変数Tの型が配列だった場合に、全てのプロパティをマップするのではなく要素の型のみをマップしてくれるのです。ではやってみましょう。

ここらへんの話、TypeScriptがすごいのとuhyoさんの説明がうまいのとでめっちゃおもしろい

hajimismhajimism

Readonly<T>の型変数Tに配列やタプルの型を入れると、readonly配列やreadonlyタプルの型になります。

なるほど

hajimismhajimism

keyof 型という型はかならずstring | number | symbolの部分型になります。keyof anyはキーとしてありえる全ての型となり、やはりstring | number | symbolになります。最初からstring | number | symbolと書いてもよいですが、将来的にキーになり得る型が追加されたときのためかこれをベタ書きするのは避けてkeyof anyとしているようです

なるほど

hajimismhajimism
/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

こう見るとnever型の表現力すごいな。0を発見したときの感動ってこんな感じだったんかな。

hajimismhajimism

これはなかなかよく使うので、これにOmit<T, K>みたいな名前を付けることも結構あるようです。

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

これいつから本体に追加されたんだろ。ExcludeしたものをPickするってなるほどだな。

hajimismhajimism

T extends (...args: any[]) => anyという条件はTが関数の型でなければいけないということを言っています

なるほど
T extends (...args: unknown[]) => unknownじゃないのか?

hajimismhajimism

自分で書いて理解


type Param<T extends (...args: any[]) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never

type Return<T extends (...args: any[]) => any> = T extends (
  ...args: any[]
) => infer R
  ? R
  : never
hajimismhajimism

読み終わった

次のステップ
「TypeScriptの型中級」はまだ書いていませんが、その代わりに入門・初級をマスターしたみなさんが次に読むと良い記事をご案内します。

TypeScriptの型演習 演習問題ができました。力試しに解いてみましょう。
TypeScriptの型推論詳説 型推論を詳しく理解したい方はこちら。
TypeScriptのreadonlyプロパティを使いこなす readonlyについて深掘りする記事です。
TypeScriptで超型安全なBuilderパターン TypeScriptの型の応用例です。
TypeScriptで配列が指定した要素を全部持つか調べる方法 TypeScriptの型の応用例です。
TypeScriptで最低一つは必須なオプションオブジェクトの型を作る これも応用例です。
TypeScriptで最低n個の要素を持った配列の型を宣言する方法 これまた応用例です。
TypeScriptの型レベル連結リスト活用術:型を変えられるコンテナを作る さらに応用例です。

次からはこれを潰していきますかー。
すごい長いスクラップになりそう。

hajimismhajimism

ひっかかったところをコメントします
https://qiita.com/uhyo/items/e4f54ef3b87afdd65546

hajimismhajimism

3-1 配列からMapを作る

自分の回答

function mapFromArray<T extends object>(
  arr: T[],
  key: keyof T
): Map<T[keyof T], T> {
  const result = new Map()
  for (const obj of arr) {
    result.set(obj[key], obj)
  }
  return result
}

uhyoさんの回答例

function mapFromArray<T, K extends keyof T>(arr: T[], key: K): Map<T[K], T> {
  const result = new Map();
  for (const obj of arr) {
    result.set(obj[key], obj);
  }
  return result;
}
hajimismhajimism

自分の回答だと下記のように推論される

const dataMap: Map<string | number, {
    id: number;
    name: string;
}>

これに対し、uhyoさんの回答例だとMapのkeyの型がより厳密に推論されている。

const dataMap: Map<number, {
    id: number;
    name: string;
}>

次元の違う引数2つに対して型引数1つで対処しているところに無理がありそう。印象の話だけど。

あとTはobjectに縛らなくていいのか...よくわからん

hajimismhajimism

3-5 undefinedな引数

回答例

type Func<A, R> = undefined extends A ? (arg?: A) => R : (arg: A) => R;

引数の型の中だけでconditional typeを使ってごにょごにょしてたので解けなかった。
あと変だなーと思いつつA extends undefinedってやってた。なるほど逆にすればよいのか。

hajimismhajimism

4-1. 無い場合はunknown

自分の回答

const getFoo = <S extends object>(
  obj: S
): S extends { foo: infer R } ? R : unknown => {
  return obj.foo
}

uhyoさんの回答例

function getFoo<T extends object>(
  obj: T
): T extends { foo: infer E } ? E : unknown {
  return (obj as any).foo;
}

return (obj as any).foo;が書けなかった。負けていいところがわかってない。

hajimismhajimism

4-3. Unionはいやだ

自分の回答


class EventDischarger<E> {
  emit<Ev extends keyof E>(
    eventName: Ev,
    payload: [Ev] extends [keyof E] ? E[Ev] : never
  ) {
    // 省略
  }
}

uhyoさんの回答例

type Spread<Ev, EvOrig, E> = Ev extends keyof E
  ? EvOrig[] extends Ev[]
    ? E[Ev]
    : never
  : never;
class EventDischarger<E> {
  emit<Ev extends keyof E>(eventName: Ev, payload: Spread<Ev, Ev, E>) {
    // 省略
  }
}

union distributionを防げばいいんだな〜というなんとなくのイメージは湧いていたけど、なんとなくの域を出なかった。一旦distributeさせて、させなかった場合と比較することで、単一unionかどうかを判定させている。

なんでSpreadってい名前がついているかよくわからない。unionを分解しているという意味?

hajimismhajimism

4-8. オプショナルなキーだけ抜き出す

回答例

type PickUndefined<Obj> = {
  [K in keyof Obj]-?: undefined extends Obj[K] ? K : never
}[keyof Obj];

type MapToNever<Obj> = {
  [K in keyof Obj] : never
}

type OptionalKeys<Obj> = PickUndefined<MapToNever<Obj>>

1回MapToNeverを経由するのうますぎる

このスクラップは2023/01/09にクローズされました