TypeScriptのレベルアップ! type-challenges の easy で学んだこと
この記事は、TypeScript Advent Calendar 2022 の15日目の記事です。
この記事の概要
TypeScript ビギナーの筆者が TypeScript をより理解するのに役立った、 type-challenges を紹介する記事です。
type-challenges はレベル分けされた問題集です。問題形式で楽しみながら学ぶことができます。
この記事の前半では、type-challenges の easy にはどんな問題があるかと、何が学べるかを紹介します。
後半では、問題を解くのによく使ったテクニックを簡単に解説します。
type-challenges easy で何が学べるか
どんな問題があるか
easy は全部で 13 問あります。(執筆時点)
課題は、以下のユーティリティ型の実装を行うことです。
-
Pick<T, K>
ユーティリティ型 (4 - Pick) -
Readonly<T>
ユーティリティ型 (7 - ReadOnly) - 配列をオブジェクト型に変換する、
TupleToObject<T>
ユーティリティ型 (11 - Tuple to Object) - 配列の最初の要素を返す、
First<T>
ユーティリティ型 (14 - First) - 配列の length を返す、
Length<T>
ユーティリティ型 (18 - Length of Tuple) -
Exclude<T, U>
ユーティリティ型 (43 - Exclude) -
Awaited<T>
ユーティリティ型 (189 - Awaited) -
If<C, T, F>
ユーティリティ型 (C
が真偽値、 truthy の場合T
を返し、 falsy の場合F
を返す )(268 - If) -
Array.concat()
の型版 (533 - Concat) -
Array.includes()
の型版 (898 - Includes) -
Array.push()
の型版 (3057 - Push) -
Array.unshift()
の型版 (3060 - Unshift) -
Parameters<T>
ユーティリティ型 (3312 - Parameters)
学んだテクニック
まず、 Generics は全ての問題で使うので、 Generics を実践練習したいという方にはもってこいです。
以下に、問題を解くのによく使ったテクニックを紹介します。ここでは、用語の紹介のみにとどめ、解説は次の章で行います。
- Generic Constraints
-
readonly
modifier - Mapped Types
- Indexed Access Types
- Conditional types
-
never
によるフィルタリング -
infer
による型推論
-
- Variadic Tuple Types
「なにそれ 🤔」ってなった用語があった方は、ぜひ type-challenges に挑戦してみてください!
よく使われたテクニックの解説
ここからは type-challenges の easy でよく使ったテクニックを簡単に解説します。
表面的な解説になりますので、詳しくは参照リンクをご確認ください。
Generic Constraints
Generic Constraints は、extends
キーワードを使って、Generics の型を特定の型に限定する方法です。
例えば以下の例のようなで役に立ちます。
例:Generics T
を型に持つ arg
が length
method を呼び出しているが、Generics は任意の方が指定可能なので(length
を持たない可能性があるので)、コンパイルエラーが起きてしまう。
function checkLength<T>(arg: T): number {
// Error: Property 'length' does not exist on type 'T'.
return arg.length;
}
このような際に、extends
キーワードで、Generics 型引数の型を特定の型に限定することができます。(Generic Constraints)
これにより安全にオブジェクトのプロパティにアクセスできるようになります。
function checkLength<T extends string>(arg: T): number {
return arg.length;
}
keyof
型演算子との組み合わせ
Generic Constraints は、 keyof
型演算子との組み合わせでもよく使います。(keyof
の詳細は以下参考リンクをご確認ください。)
以下の例では、getProperty
関数の第二引数 key
を K extends keyof T
とすることによって、T
型( x
オブジェクトのプロパティキーのユニオン型)に存在するもののみを受け付けるように型制限をしています。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
// K => "a" | "b" | "c" | "d"
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
// Error: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
readonly
modifier
readonly
modifier をつけることで、配列や、オブジェクトのプロパティをイミュータブルにすることができます。
例: obj
foo への代入でエラーが起きる。
let obj: {
readonly foo: number;
};
obj = { foo: 1 };
obj.foo = 2;
// Error: Cannot assign to 'foo' because it is a read-only property.
例: a
への代入や、要素の変更でエラーが起きる。
let a = readonly ['a', 'b', 'c'];
a.push('d'); // error
a[0] = 'x'; // error
Mapped Types
Mapped Types は、ユニオン型に対して in
キーワードを使うことで、キーを反復して型を作成するテクニックです。
以下例では、AvialbleLanguage
ユニオン型に対して in
キーワードを使って、HelloTranslation
オブジェクト型を作成しています。
type AvailableLanguage = 'en' | 'jp';
type HelloTranslation = {
[key in AvailableLanguage]: string;
}
/**
* type HelloTranslation = {
* en: string;
* jp: string;
* }
*/
const hello: HelloTranslation = {
'en': 'hello',
'jp': 'こんにちは',
'fr': 'bonjour',
/**
* Error: Type '{ en: string; jp: string; fr: string; }' is not assignable to type 'HelloTranslation'.
* Object literal may only specify known properties, and ''fr'' does not exist in type 'HelloTranslation'.
*/
}
ユニオン型に対してマッピングを行うため、keyof
型演算子でオブジェクト型のプロパティをユニオン型にしてから、in
で反復するというテクニックもよく使われます。
Indexed Access Types
Indexed Access Types は、JavaScript のオブジェクトのプロパティにアクセスする方法と似ています。
以下の例では、Cat
オブジェクト型の、プロパティの型にアクセスしています。
type Cat = { age: number; name: string; alive: boolean };
type Age = Cat["age"];
// type Age = number;
アクセスするインデックス型は、それ自体が型なので、ユニオン型を使って複数のプロパティにアクセスすることもできます。
type Cat = { age: number; name: string; alive: boolean };
type Type1 = Cat["age" | "name"];
// type Type1 = string | number
type Type2 = Cat[keyof Cat];
// type Type2 = string | number | boolean
また、Indexed Access Type の便利なテクニックとして、タプル型に対して [number]
でアクセスする方法があります。
これにより、タプル型の要素をユニオン型として取得することができます。
type Animals = ['cat', 'dog', 'tiger', 'lion', 'elephant'];
type AnimalsUnion = Animals[number];
// type AnimalsUnion = 'cat' | 'dog' | 'tiger' | 'lion' | 'elephant'
ユニオン型を生成するので、上記の Mapped types と組み合わせることで威力を発揮する便利なテクニックです。
Conditional types
Conditional types は、JavaScriptの ?
を使った Ternary operator に似た形で、extend
と ?
を使って条件つきの型を作成できます。
type True = true extends boolean ? true : false;
never
によるフィルタリング
発生し得ない値を意味する never
を、 Conditional types のリターンとして使うことで、型の絞り込みができます。
以下の例では、AvailableLanguage
に含まれないユニオン型の要素は、never
により除外されています。
type AvailableLanguage = 'en' | 'jp';
type FilteredLanguage<T> = T extends AvailableLanguage ? T : never;
type Foo = FilteredLanguage<'en' | 'fr' | 'jp' | 'cn' | 'kr'>;
// type Foo = "en" | "jp"
infer
による型推論
infer
は Conditional types で、条件に合致した型を推論するために使われます。
以下の例では、関数の返り値の型を推論し、T
が関数型である場合に、その返り値を infer
で取得しています。
type GetReturnType<T> = T extends (...args: unknown[]) => infer Return
? Return
: never;
type Num = GetReturnType<() => number>;
// type Num = number
type Str = GetReturnType<(x: string) => string>;
// type Str = string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
// type Bools = boolean[]
Variadic Tuple Types
Variadic Tuple Typesは、JavaScriptのスプレッド構文の型バージョンのようなものです。
type Strings = [string, string];
type Numbers = [number, number];
type StrStrNumNumBool = [...Strings, ...Numbers, boolean];
// type StrStrNumNumBool = [string, string, number, number, boolean]
まとめ
この記事では、 type-challenges の easy でどんなことが学べるかをまとめてみました。
TypeScript の勉強方法を探している方、ハンズオンで楽しく学べる type-challenges おすすめです!
各問題の回答、解説もしっかりしているので、ぜひチャレンジしてみてください!
This article is also available in English: https://dev.to/takuyakikuchi/level-up-on-typescript-what-i-learned-from-type-challenges-4o60
Discussion