Open19

Type Challenges 1〜

saneatsusaneatsu

はじめに

Type Challengesとは?

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

このリポジトリ内にあるTypeScriptの型定義に関する問題(全179問)。
アルゴリズム系の問題はよくあるけど型定義の問題あるのとても良い👏🏻
日本語のドキュメント も用意してある。

例えば、Utility typeのPickを自前実装する問題とかがある。

手元で環境を整えずとも、ブラウザからアクセスできるTypescriptのPlaygroundでお手軽に実行できる。

https://www.typescriptlang.org/play/

モチベーション

ちょうど今、社内プロダクト開発時にGenerics、Conditional Type、inferを使い倒した複雑な型定義をしているが、未だに苦手意識があるので解いてみながらなくしていきたい。

「読んだらなんとなくわかる」から「ゼロから書ける」状態になりたい。

saneatsusaneatsu

1. Hello World(Warm-up)

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00013-warm-hello-world/README.md

解答

/* _____________ Your Code Here _____________ */
- type HelloWorld = any // expected to be a string
+ type HelloWorld = srting // expected to be a string

に書き換えるだけの問題。

/* _____________ Test Cases _____________ */
import type { Equal, Expect, NotAny } from '@type-challenges/utils'

type cases = [
  Expect<NotAny<HelloWorld>>,
  Expect<Equal<HelloWorld, string>>,
]

独自の型を定義して、それを配列に入れたものをテストケースとしている。
なるほどなぁ。

Equal の中身

https://zenn.dev/yumemi_inc/articles/ff981be751d26c
https://zenn.dev/razokulover/articles/890102685d5ea2

遅延評価なんてTtypeScript書きながら考えたことないな。。

改めて勉強

どちらもuhyoさんの記事。
読んで一発で理解、は到底無理なので何度も参照したい。

https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a
https://qiita.com/uhyo/items/da21e2b3c10c8a03952f

saneatsusaneatsu

2. Pick(Easy)

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.md

解答

type MyPick<T, K extends keyof T> = {
  [key in K]: T[key]
}

メモ

https://zenn.dev/oreo2990/articles/79430780d42203
https://zenn.dev/oreo2990/articles/1040312d7af066

Equalの挙動を調べる際に復習がてらここらへんを眺めていたので extends keyof はすっと出てきた。
右辺がわからなかった。

https://github.com/type-challenges/type-challenges/issues/13427
Goodが多かったここからコードを拝借。

type name = 'firstname' | 'lastname'
type TName = {
  [key in name]: string
}
// TName = { firstname: string, lastname: string }

extends keyofin keyof の違い

上のIssueで取り上げられていたので改めて調べてみる。

extends keyof

  • ある型のプロパティのキーを使って型制約を設けるために使われる。
  • 具体的には、型引数が特定のオブジェクト型のプロパティ名であることを保証する。
type Person = {
  name: string;
  age: number;
};

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person: Person = { name: 'Alice', age: 30 };

const name = getProperty(person, 'name'); // ✅ Success
const age = getProperty(person, 'age'); // ✅ Success
// const invalid = getProperty(person, 'height'); // 🚫 Error

in keyof

  • オブジェクト型のプロパティ名を列挙して新しい型を定義するために使われる。
  • 具体的には、マッピング型(Mapped Type)を作成する際に使用される。

https://zenn.dev/oreo2990/articles/249fa60986aad5
https://zenn.dev/qnighy/articles/dde3d980b5e386

マッピング型というのは { [P in K]: T } という形。

// OriginalTypeのすべてのキーKに対してTransformationを適用し、新しい型MappedTypeを定義する
type MappedType = {
  [K in keyof OriginalType]: Transformation;
};

https://stackoverflow.com/questions/57337598/in-typescript-what-do-extends-keyof-and-in-keyof-mean

in はstring, number, symbol literalの和で型付けしたいインデックスシグネチャを定義する時に使う。
keyofと組み合わせることで、元の型のすべてのプロパティを再マッピングした、いわゆるマップされた型を作成することができる。

インデックスシグネチャってなんだっけ。

https://zenn.dev/tsuboi/articles/ee43ddc5a2cd941de138

インデックスシグネチャの定義は
{[key:T]:U}・・・このオブジェクトについては型Tの全てのキー(プロパティ名)は、型Uの値を持たなければならないことを意味します。
型Tは'string' または 'number' のいずれかである必要があります。

今回の解答の[key in K]: の部分はまさにインデックスシグネチャなのか。
K extends keyof T ということは KT のキーなのでTに何が来てもKstring | numberという推論が成り立っているということなんだろうか。

type Person = {
  name: string;
  age: number;
};

// すべてのプロパティをオプショナルにする
type PartialPerson = {
  [K in keyof Person]?: Person[K];
};

const partialPerson: PartialPerson = { name: 'Alice' }; // 部分的にプロパティが設定されている

// すべてのプロパティの型を変更する
type ReadOnlyPerson = {
  [K in keyof Person]: Readonly<Person[K]>;
};

const readOnlyPerson: ReadOnlyPerson = { name: 'Alice', age: 30 }; // ✅ Success
// readOnlyPerson.name = 'Bob'; // 🚫 Error

頭こんがらがる。

saneatsusaneatsu

3. Readonly(Easy)

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00007-easy-readonly/README.md

解答

Readonly<T> が使えない制限ならreadonly を全てのプロパティに適用させるコードを書けば良いのですぐわかった。
とはいえ、これがすぐわかったの1つ前の問題を解いていたからだな。

インデックスシグネチャでマッピング型を作成(学習した言葉をすぐ使う)。

type MyReadonly<T> = {
  readonly [key in keyof T] : T[key]
}

メモ

https://zenn.dev/qnighy/articles/dde3d980b5e386#readonly

そういやマッピング型について調べてる時に +readonly とか出てきたんだけどこういうのも使っていくのだろうか。

saneatsusaneatsu

4. Tuple to Object(Easy)

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00011-easy-tuple-to-object/README.md

解答

[key in T]: key まで書いた後に T にエラーが出ていて、インデックスで要素にアクセスすればいいから...とT[number] をしてみたら通った。

// type TupleToObject<T extends readonly PropertyKey[]> ={ // 👍 Better
type TupleToObject<T extends readonly any[]> ={
  [key in T[number]]: key
}

ref: インデックスアクセス型 (indexed access types) | TypeScript入門『サバイバルTypeScript』

メモ

Tuple型

  • 長さが固定されている
  • 型が決定している
    • 各要素の型があらかじめ決められており、その順序も固定されている
    • 例えば、[number, string] というタプルは、最初の要素が number、次の要素が string である必要がある

PropertyKey

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

// type PropertyKey = string | number | symbol;

type TupleToObject<T extends readonly PropertyKey[]> ={
  [key in T[number]]: key
}

PropertyKey 使ってる方がいいな。

Symbol型って存在はしっているものの使い道がわかってない。

https://marsquai.com/a70497b9-805e-40a9-855d-1826345ca65f/1dc3824a-2ab9-471f-ad58-6226a37245ce/b18ffd4a-6899-439a-9e94-cd456f6c5f75/

うっすらわかった気になった。

saneatsusaneatsu

5. First of Array(Easy)

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00014-easy-first/README.md

解答

type First<T extends any[]> = T extends [] ? never : T[0]

メモ

最初はなんとなく以下のように書いたらneverだけでエラーが出た。
三項演算子で回避して解決。

type First<T extends any[]> = T[0]

type cases = [
  Expect<Equal<First<[3, 2, 1]>, 3>>,
  Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>,
  Expect<Equal<First<[]>, never>>, // 🚫 Error
  Expect<Equal<First<[undefined]>, undefined>>,
]
saneatsusaneatsu

6. Length of Tuple(Easy)

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00018-easy-tuple-length/README.md

解答

type Length<T extends readonly string[]> = T['length']

メモ

最初はこんなかんじで書いた。

type Length<T extends any[]> = T.length

Cannot access 'T.length' because 'T' is a type, but not a namespace. Did you mean to retrieve the type of the property 'length' in 'T' with 'T["length"]'?

というエラーが.lengthで出ているので言われた通りT["length"]にしてみる。

Type 'readonly ["tesla", "model 3", "model X", "model Y"]' does not satisfy the constraint 'string[]'.
The type 'readonly ["tesla", "model 3", "model X", "model Y"]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.

次はtypeofの箇所でこのようなエラーが出た。
ミュータブルな型になっているのでreadonlyを付けて完成。

saneatsusaneatsu

7. Exclude(Easy)

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00043-easy-exclude/README.ja.md

解答

type MyExclude<T, U> = T extends U ?  never : T

メモ

ユニオン型

| で繋いでるからといって、ユニオン型ではないのか?
type Example = string | number みたいなやつだよな。

TypeScriptのユニオン型(union type)は「いずれかの型」を表現するものです。

ref: ユニオン型 (union type) | TypeScript入門『サバイバルTypeScript』

いや、以下のようなコードもあるからこれもある意味ユニオン型とも言えるのか。

type ErrorCode = 400 | 401 | 402

type Example = 'a' | 'b' | 'c' というふうにも書けるわけだし。

<T, U> の方

type MyExclude<T, U> = anyって最初から書いてあるけどユニオン型をTで受け取ったとして...?
別にUに入ってくるものをTで保証する必要とか特に無いから<T, U extends T> 的なのも特に書く必要はないはず。

any の方

多分こっちをどうにかせねばな感じがする。 
Uの中でT をextendsしてるものを返すことになるので、U extends T ? U : never としてみたけど違いそう。

ん?なんか脳みそでIncludeを考えてた。真逆だ。
最終的に返すのはTの値なので三項演算子は neverT にして以下解答。

type MyExclude<T, U> = T extends U ?  never : T
saneatsusaneatsu

8. Awaited(Easy)

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00189-easy-awaited/README.md

解答

わからなくて答え見た。

type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer U>
  ? U extends PromiseLike<any>
    ? MyAwaited<U>
    : U
  : never;
type MyAwaited<T> = T extends PromiseLike<infer U> ? MyAwaited<U>  : T

誤答

type MyAwaited<T> = T extends Promise<infer U> ? U : never;

type cases = [
  Expect<Equal<MyAwaited<X>, string>>, // ✅
  Expect<Equal<MyAwaited<Y>, { field: number }>>, // ✅
  Expect<Equal<MyAwaited<Z>, string | number>>, // 🚫
  Expect<Equal<MyAwaited<Z1>, string | boolean>>, // 🚫
  Expect<Equal<MyAwaited<T>, number>>, // 🚫
]

最初こんな感じにしてみたが、テストが通らないものがあった。

メモ

https://zenn.dev/brachio_takumi/articles/464106a6a80eca8ab919

infer調べてたらこの記事に遭遇した色々勉強する。

型の勉強をするとなんか聞いたことあるけどいまいちピンときてない言葉で、とてもよくわからない言葉の説明をされてしまうのどうにかせねば。

アサーション

  • 型アサーション
    • as hoge
  • constアサーション
    • as const。readonlyが付与される

assertionは「主張・言明」の意。

データ型

データ型は2種類ある

  • プリミティブ型
    • イミュータブル(値を直接変更できない)
    • プロパティを持たない
    • 7種類
      • boolean型(論理型): trueまたはfalseの真偽値。
      • number型(数値型): 0や0.1のような数値。
      • string型(文字列型): "Hello World"のような文字列。
      • undefined型: 値が未定義であることを表す型。
      • null型: 値がないことを表す型。
      • symbol型(シンボル型): 一意で不変の値。
      • bigint型(長整数型): 9007199254740992nのようなnumber型では扱えない大きな整数型。
  • オブジェクト
    • ミュータブル(変更可能)
    • プロパティを持つ

Q:string型は"name".lengthの形式でプロパティを持つのでは?
A:JavaScriptにはプリミティブ型をオブジェクトに自動変換するオートボクシングという機能があるため、プリミティブ型でもまるでオブジェクトのように扱える

リテラル型

  • プリミティブ型の特定の値だけを代入可能にする型のこと
  • 一般的にリテラル型はマジックナンバーやステートの表現に用いられます。その際、ユニオン型と組み合わせることが多い
  • リテラル型として表現できるプリミティブ型は3種類
    • boolean型のtrueとfalse
    • number型の値
    • string型の文字列
// 例1
let x: 1;
x = 1; // プリミティブ型→リテラル型になる
x = 100;

// 例2
const isTrue: true = true;
const num: 123 = 123;
const str: "foo" = "foo";

// 例3
let num: 1 | 2 | 3 = 1; // ユニオン型との組み合わせ

Widening Literal Types、NonWidening Literal Types

リテラル型における推論方法の違い。
リテラル型がより一般的な型に「拡張」されるかどうかを制御する。

  • Widening Literal Types
    • Widening Literal Types は、リテラル型がより広い型(例えば string や number)に自動的に変換(拡張)されることを指します。これは特に変数宣言時に発生します。
    • // ここで x の型は string 型として推論される。よって後から"hello"以外の文字列を代入できる
      let x = "hello"; 
      
  • NonWidening Literal Types
    • リテラル型がそのまま特定のリテラル型として保持され、より広い型に自動的に変換されないことを指す。
    • 特に const 宣言や明示的な型注釈を用いる場合に発生する
    • // ここで y の型は "hello" というリテラル型として推論されるため"hello"以外代入できない
      const y = "hello"; 
      
    • ※ 調査不足だけどただ単にconstで定義しただけではどっちのリテラル型でもあり、constアサーションを使った時にNonWidening Literal Typesになるとこの記事では書いてあった

infer

  • 直訳すると「推論」の意。
  • Conditional Type(type IsNumber<T> = T extends number ? true : false; みたいな)で使われるもの
    • 使える場所はextendsの条件部分に限定されている
// T が id というプロパティを持っている場合は、その id の型を返す。
// もし、id というプロパティがなければ never を返す。
type Id<T> = T extends { id: infer U } ? U : never;

ChatGPTにはまさにPromiseのことを教えられた。

ReturnType

inferの例としてUtility typeのReturnTypeがよく取り上げられていたので勉強しておく。
要は関数を入れたらその返り値の型を取得してくれるやーつ。

function greet(): string {
    return "Hello, TypeScript!";
}

type GreetingType = ReturnType<typeof greet>; // = string
const user = {
    id: 1,
    name: "Taro",
    getInfo() {
        return {
            id: this.id,
            name: this.name
        };
    }
};

type UserInfoType = ReturnType<typeof user.getInfo>; // { id: number; name: string; }
async function fetchData(): Promise<number> {
    return 10;
}

// これだとPromise<number>が返ってくる
// type FetchedData = ReturnType<typeof fetchData>;

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type FetchedData = UnwrapPromise<ReturnType<typeof fetchData>>;

https://jp-seemore.com/web/13906/

このサイトは実例が色々載っていてわかりやすかった。高度なマッピング型の例とか今後Type Challengeでも出てきそう。

https://zenn.dev/okunokentaro/articles/01gm2prv1mkqp13k3wxyg9b2bg

この人は「TypeScript一人カレンダー」をやっているので他の記事も見てみる。

PromiseLike

恥ずかしながら解答見て始めて存在を知った。

https://qiita.com/suin/items/b9d00dff380486338ecd

saneatsusaneatsu

9. If(Easy)

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00268-easy-if/README.md

解答

type If<C extends boolean, T, F> = C extends true ? T : F

メモ

過去一簡単だった。

type If<C extends boolean, T, F> = C extends true ? T : F

import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<If<true, 'a', 'b'>, 'a'>>,
  Expect<Equal<If<false, 'a', 2>, 2>>,
  Expect<Equal<If<boolean, 'a', 2>, 'a' | 2>>, // これ
]

1つずつテスト通してくか、の精神でなんとなく書いたらテストが通ってしまった。
なんでこの場合3つ目のテストがOKになるんだと思ったけど、boolean型の場合Ctrue false どちらにもなりうるから両方のケースがカバーされて'a' | 2になるということか。

saneatsusaneatsu

10. Concat(Easy)

問題

解答

type Concat<T extends readonly any[], U extends readonly any []> = [...T, ...U]

経緯

まずはシンプルに書いてみる。

type Concat<T extends any[], U extends any []> = [...T, ...U]

Type 'readonly [1]' does not satisfy the constraint 'any[]'.
  The type 'readonly [1]' is 'readonly' and cannot be assigned to the mutable type 'any[]'.

readonly [1] 型は any[] ではない、と。
tupleの型ってany型に入らないのか?

調べてみるとReadonlyArray<T> readonly T[]という書き方があるじゃないか(2つに違いはない)。
これを付け足して解決。

https://typescriptbook.jp/reference/values-types-variables/array/readonly-array

メモ

別解を見る

別解ないかとりあえず一番評価されているのを見てみる。

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

type Tuple = readonly unknown[];
type Concat<T extends Tuple, U extends Tuple> = [...T, ...U];

anyunknown の違いを明確に言語化できるように改めて勉強しよう。

anyunknown

  • any
    • 説明
      • 型安全性を無視し、あらゆる操作が許容される
    • ユースケース
      • 型チェックを回避したいときに使用
      • 乱用すると型安全性が失われる
    • 使用例
      •  let value: any;
         value = 42;
         value = "hello";
         value = { key: "value" };
        
         // 型チェックが無効
         value.toUpperCase(); // エラーなし
         value.someMethod(); // エラーなし
        
  • unknown
    • 説明
      • 型安全性を保ち、操作の前に型チェックが必要
        • つまり、値の型が確定するまで操作を行うことができない
      • 任意の値が割り当て可能だが、anyよりも型安全性を維持する
    • ユースケース
      • 型安全性を維持しつつ、任意の値を扱う必要がある場合に使われる
      • 特に、外部からの入力や動的なデータの処理に適している
    • 使用例
      •  let value: unknown;
         value = 42;
         value = "hello";
         value = { key: "value" };
        
        // 型チェックが有効
         if (typeof value === "string") {
           value.toUpperCase(); // OK
         } else {
           // value.toUpperCase(); // エラー: 型 'unknown' にプロパティ 'toUpperCase' は存在しません
         }
         
         if (typeof value === "object" && value !== null) {
           // 型アサーションを使う
           console.log((value as { key: string }).key); // OK
         }
        
saneatsusaneatsu

11. Includes(Easy)

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00898-easy-includes/README.md

解答

パターン1

うわ〜。T extends [infer First, ...infer Last] とかまんま見たことあるな〜。
あと、importしてるEqualを使うとか1mmも考えなかった。

何日か後にもう一度やろう。

type Includes<T extends readonly any[], U> = T extends [infer First, ...infer Last] ? 
    Equal<U, First> extends true ? 
        true : 
        Includes<Last, U> : 
    false
  • T extends [infer First, ...infer Last]
    • T が少なくとも1つの要素を持つ配列に分解できるかをチェックする
    • Firstは配列の最初の要素に割り当てられ、Restは残りの要素に割り当てらる
  • Equal<U, First> extends trueは、UFirstが同じかどうかをチェック
  • Includes<Rest, U>
    • 異なる場合は、再帰的に残りの要素の中でUを探す
  • 再帰していきTが空配列になると再帰が終了しfalseを返す

パターン2

type MyEqual<X, Y> = (<T>() => T extends X ? 1 : 2 ) extends (<T>() => T extends Y ? 1 : 2) ? true : false
type Includes<T extends readonly any[], U> = true extends {
  [I in keyof T]: MyEqual<T[I], U>
}[number] ? true : false
  • { [I in keyof T]: MyEqual<T[I], U> }
    • 配列Tの各要素とUを比較するマッピング型
    • keyof Tは、配列Tのすべてのインデックスキーを取得する
    • T[I]は、Tの各インデックスIに対応する要素の型を取得する
    • MyEqual<T[I], U>は、各要素T[I]Uと等しいかどうかをチェックする
  • { [I in keyof T]: MyEqual<T[I], U> }[number]
    • マッピング型のすべての値をユニオン型として取得する
    • 例えば、T['a', 'b', 'c']であり、U'a'である場合、これはMyEqual<'a', 'a'> | MyEqual<'b', 'a'> | MyEqual<'c', 'a'>というユニオン型になる
  • true extends { [I in keyof T]: MyEqual<T[I], U> }[number]
    • ユニオン型にtrueが含まれているかどうかをチェックする
    • もし含まれていれば、全体としてtrueが返され、含まれていなければfalseが返される

メモ

正解にたどり着けず

// type Includes<T extends readonly any[], U> = any // 初期状態
type Includes<T extends readonly any[], U> = U extends T ? true : false

まずは一旦こんな感じに。

結果がfalse のものは通ったがtrueのものはすべて通らず。
U extends T が違うんだろうな〜。

infer をいい感じに使いたいんだけどわからん。
inとかもどうやら使えないし...。

で、ギブアップ。

saneatsusaneatsu

17. Readonly 2(Middle)

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00008-medium-readonly-2/README.md

解答

& の後からわからんかった...。Middleの時点で結構むずいな。
as Pとか1個前でも使われてるし今一度復習してから次に行かないと身につかなさそう...。

type MyReadonly2<T, K extends keyof T = keyof T> = {
  readonly[P in K] : T[P] 
} & {
  [P in keyof T as P extends K  ? never: P] : T[P]
}