🧩

TypeScriptのレベルアップ! type-challenges の easy で学んだこと

2022/12/15に公開

この記事は、TypeScript Advent Calendar 2022 の15日目の記事です。

この記事の概要

TypeScript ビギナーの筆者が TypeScript をより理解するのに役立った、 type-challenges を紹介する記事です。
type-challenges はレベル分けされた問題集です。問題形式で楽しみながら学ぶことができます。

この記事の前半では、type-challenges の easy にはどんな問題があるかと、何が学べるかを紹介します。

後半では、問題を解くのによく使ったテクニックを簡単に解説します。

https://github.com/type-challenges/type-challenges

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 を型に持つ arglength 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;
}

https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints

keyof 型演算子との組み合わせ

Generic Constraints は、 keyof 型演算子との組み合わせでもよく使います。(keyof の詳細は以下参考リンクをご確認ください。)
以下の例では、getProperty 関数の第二引数 keyK 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"'.

https://www.typescriptlang.org/docs/handbook/2/keyof-types.html

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

https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#readonly-and-const

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 で反復するというテクニックもよく使われます。

https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#handbook-content

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 と組み合わせることで威力を発揮する便利なテクニックです。

https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html

Conditional types

Conditional types は、JavaScriptの ? を使った Ternary operator に似た形で、extend? を使って条件つきの型を作成できます。

type True = true extends boolean ? true : false;

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#handbook-content

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"

https://www.typescriptlang.org/docs/handbook/2/functions.html#never

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[]

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types

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]

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#variadic-tuple-types

まとめ

この記事では、 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

GitHubで編集を提案

Discussion