夏バテしながら「Type Challenges 初級編」やってみた
夏バテしながら「Type Challenges」をより課題に取り組みやすいようにしてくれている「mosya<TC>」 の初級編に取り組んでみました。
個人的にTypeScriptがなぜTypeの文字を含んでいるのか理解する足掛かりにはなる教材だと思っています。基本文法は分かったけど、TypeScriptって何が便利なんだ...という方におすすめです。
以下は内容です。
初級編
1.Hello World
問題
any型として定義されているHelloWorldをstring型として定義してみましょう。
// HelloWorldを文字列型にしてください
type HelloWorld = any;
回答
// HelloWorldを文字列型にしてください
type HelloWorld = string;
メモ
TypeScriptの型エイリアスを用いてHelloWorldという型に、string型を割り当てている。
型に名前を新しくつけているので、以下のように使用できる。
let greeting: HelloWorld = "Hello World";
2.Readonly型を定義してみよう
問題
組み込みの型ユーティリティReadonly< T >を使用せず、T のすべてのプロパティを読み取り専用にする型を実装してみよう。実装された型のプロパティは再割り当てできません。
// MyReadonly<T>を実装してください
// MyReadonlyは、Tのプロパティをすべて読み取り専用にする型です
interface Todo {
title: string
description: string
}
const todo: MyReadonly<Todo> = {
title: "Hey",
description: "foobar"
}
回答
// MyReadonly<T>を実装してください
// MyReadonlyは、Tのプロパティをすべて読み取り専用にする型です
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}
interface Todo {
title: string
description: string
}
const todo: MyReadonly<Todo> = {
title: "Hey",
description: "foobar"
}
メモ
TypeScriptのMapped Typesを用いて、既存の型から新しい型を生成している。またオブジェクトのキーを取得するためにkeyofを使用し、オブジェクトの型を取得するためにインデックスアクセス型も使用している。
- Mapped Types
例は以下。K in Todokeys
はループ文を回してるイメージかな。
type TodoKeys = "title" | "description"
type Todo = {
[K in TodoKeys]: string
}
//以下のように展開される
type Todo = {
title: string
description: string
}
- key of
オブジェクトのキーを取得するキーワード。keyof T
とするとT
のキーを取得できる。
inter fase Todo {
title: string
description: string
}
type TodoKeys = keyof Todo; // "title" | "description"
- インデックスアクセス型
オブジェクトのプロパティにアクセスするための機能。
interface Todo {
title: string
description: string
}
type Title = Todo["string"] // string
3.タプル型の長さを返すLength< T >を作ろう
問題
タプルTを受け取り、そのタプルの長さを返す型Length<T>を実装してください。
// タプル型の長さを返すLength<T>を実装してください。
type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']
type teslaLength = Length<tesla> // expected 4
type spaceXLength = Length<spaceX> // expected 5
回答
// タプル型の長さを返すLength<T>を実装してください。
type Length<T extends any[]> = T["length"];
type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']
type teslaLength = Length<tesla> // expected 4
type spaceXLength = Length<spaceX> // expected 5
メモ
タプル型(配列型)から要素の長さを取得するには、T['length']
というインデックスを使用する。使用例は以下。
※このインデックスを使用できるように、T
を T extends any[]
と宣言して配列を受けるようにしてる。
type tesla = ['tesla', 'model 3', 'model x', 'model y'];
type teslaLength = tesla['length']; // 4
この方法で得られる長さは静的なものだから、動的なものには対応できないみたい。
⇒ 動的なものはJavaScriptで対応する。tesla.length
とか。
4.タプル型からオブジェクトを生成する型を作ろう
問題
タプルを受け取り、その各値のkey/valueを持つオブジェクトの型に変換する型を実装してください。
// Tuple型からObject型を生成するTupleToObjectを実装してください。
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type result = TupleToObject<typeof tuple> // expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
回答
// Tuple型からObject型を生成するTupleToObjectを実装してください。
type TupleToObject<T extends readonly any[]> = {
[K in T[number]]: K
}
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
メモ
Mapped Typesを数字の型に利用してオブジェクト型を生成している。また、as constの配列を型パラメータとして受け取れるようにreadonly修飾子を利用している。
- Mapped Types
type tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const;
type Cars = typeof tuple[number]; // 'tesla' | 'model 3' | 'model X' | 'model Y'
- typeof
既存の変数やオブジェクトから新しい型を作り出すために使用される。コンパイル時に変数やオブジェクトの型を参照する。使用例は以下。
let person = { name: "Alice", age: 25 };
// 'typeof person' は { name: string, age: number }型を指す。
let anotherPerson: typeof person;
anotherPerson = { name: "Bob", age: 20 }; //OK
anotherPerson = { name: "Charlie" }; //Error!!
- 索引型(Indexed type)
型が配列またはタプル型のときに、配列の各要素の型を取得する。
type ArrayType = string[];
type ElementType = ArrayType[number]; // stringになる
type Tuple = [string, number];
type ElementType = Tuple[number]; // string | numberになる
5.タプル型に要素を追加するPush型を実装する
問題
Array.pushのジェネリックバージョンを実装してください。
// Push型を定義してください
// 例えば、Push<[1, 2], '3'>は[1, 2, '3']を返します
type Result = Push<[1, 2], '3'> // [1, 2, '3']
type Push<T> =
回答
// Push型を定義してください
// 例えば、Push<[1, 2], '3'>は[1, 2, '3']を返します
type Result = Push<[1, 2], '3'> // [1, 2, '3']
type Push<T extends any[], U> = [...T, U];
メモ
スプレッド演算子の利用とT extends any[]
で配列しか受け取れないように制約をかけるのがポイント。
- スプレッド演算子
スプレッド演算子...
は配列やオブジェクトの中身を展開する演算子。
type Foo = [1, 2, 3];
type Bar = [...Foo, 4]; // [1, 2, 3, 4]
6.Unshift:タプル型の先頭に要素を追加する型を作ろう
問題
Array.unshiftの型バージョンを実装してください。
// Unshiftを実装してください
type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
回答
// Unshiftを実装してください
type Unshift<T extends any[], U> = [U, ...T];
type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
7.Pick:オブジェクト型の一部を選択する型を作ろう
問題
組み込みの型ユーティリティPick<T, K>を使用せず、TからKのプロパティを抽出する型を実装してください。
interface Todo {
title: string
description: string
completed: boolean
}
/* _____________ ここにコードを記入 _____________ */
type MyPick<T, K> = any
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
回答
interface Todo {
title: string
description: string
completed: boolean
}
/* _____________ ここにコードを記入 _____________ */
type MyPick<T, K extends string | number | symbol> = {
[k in K]: k extends keyof T ? T[k] : never;
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
メモ
Mapped TypesでKの型を1個ずつ参照し、参照したプロパティに三項演算子を用いて型付けしている。k extends keyof T
みたいな感じでTのプロパティを継承しているか確認するのがミソ。
むずかしい。明日もここでいいかも。
- Conditional Types
三項演算子かな。
type Foo<T> = T extends string ? string : number;
type FooString = Foo<string>; // string
type FooNumber = Foo<boolean>; // number
- never型
この型を使用すると型をないものとして扱える。
type Exclude<T, U> = T extends U ? never : T;
type Foo = Exclude<string | number | boolean, boolean>; //string | number
- symbol型
ちょっとよくわかんない。⇚ プロパティキーは通常 stirng | number | symbol のいずれかでなかればならないから。回答のextendsしてる。
const person = {
name: 'Alice', // "name"はプロパティキー string
age: 30, // "age"はプロパティキー number
[Symbol('id')]: 1234 // "Symbol('id')はプロパティキー Symbol
}
8.If:条件分岐する型を作ろう
問題
条件値C、 Cが truthy である場合の戻り値の型T、Cが falsy である場合の戻り値の型Fを受け取るIfを実装します。
// type Ifを実装してください。
type A = If<true, 'a', 'b'>; // expected to be 'a'
type B = If<false, 'a', 'b'>; // expected to be 'b'
回答
// type Ifを実装してください。
type A = If<true, 'a', 'b'>; // expected to be 'a'
type B = If<false, 'a', 'b'>; // expected to be 'b'
type If<C extends boolean, T, F> = C extends true ? T : F;
// type If<T extends boolean, U extends string, K extends string> = T extends true ? U : K;
メモ
Conditional Types(三項演算子?)と用いる。
9.Awaited:Promise型の中身を取り出す型を作ろう
問題
Promise likeな型が内包する型を実装してください。例えば:Promise< ExampleType >という型がある場合、どのようにして ExampleType を取得すればよいでしょうか。
type MyAwaited = any
type ExampleType = Promise<string>
type Result = MyAwaited<ExampleType> // string
回答
type MyAwaited<T> = T extends Promise<infer U> ? U : never;
type ExampleType = Promise<string>;
type Result = MyAwaited<ExampleType>; // string
メモ
inferキーワードを用いて、Promise型の引数を型推論して型を作成する。
- infer
inferキーワードはジェネリック型の中で型推論する際に使用できる。例は以下。
type ArrayItem<T> = T extends (infer R)[] : R ? never;
type Foo = ArrayItem<string[]>; // string
10.Concat:配列同士を結合する型を作ろう
問題
JavaScriptのArray.concat関数のような型を実装して下さい。
type Concat = any;
type Result = Concat<[1], [2]>; // expected to be [1, 2]
回答
type Concat<T extends any[], U extends eny[]> = [...T, ...U];
type Result = Concat<[1], [2]>; // expected to be [1, 2]
メモ
スプリット演算子を使用して配列やオブジェクトの中身を展開するのがポイント。
11.First:配列の最初の要素を返す型を作ろう
問題
配列Tを受け取り、その最初のプロパティの型を返すFirst<T>を実装してください。
// Firstを実装してください
type First = any;
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3
回答
// Firstを実装してください
type First<T extends any[]> = T extends [infer U, ...any[]] ? U : never;
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3
メモ
型を推論できるinfer
キーワードとスプレッド演算子...
を有効活用する。inferむずいな...
12.Include:配列に要素が含まれればtrueを返す型を作ろう
Discussion