Open21

type-challengesやってみる

フロントエンドえんじにゃーフロントエンドえんじにゃー

手順

  1. READMEから挑戦した問題を選ぶ
    バッジをクリックすると問題詳細ファイルに移動

  2. 「TS 挑戦する」ボタンを押すと、TypeScript Playgroundで問題にチャレンジすることができる

  3. 回答を共有 or 解答を確認

Pick課題をやってみる

Pick<T, Keys>のユーティリティ関数の挙動自体を知らなかった。。
TS弱者を自覚。。(今まで雰囲気でTSを書いていた。)
Tの型からKeysで指定したキーだけを含むオブジェクト型を返すユーティリティ型とのこと。

type MyPick<T, K> = any

easyはTSの基本文法を知る為の問題らしい。
文法的知識がないから取り掛かりがわからん。。

PickよりもIfが良いみたい。
先にそっちを解こう。

フロントエンドえんじにゃーフロントエンドえんじにゃー

If

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

Ifで必要な知識は以下とのこと

型引数の制約
Conditional Types

型引数の制約

要約:ジェネリクスので受け取る型を制限する方法 extends を使う

ジェネリクス型引数で直面する問題

function changeBackgroundColor<T>(element: T) {
  element.style.backgroundColor = 'red'
  // Property 'style' dose not exist on type 'T'
  return element
}

ジェネリクス型Tは任意の型の指定が可能になっているコードの為、
上記はエラーで失敗する。
渡す型によっては、styleプロパティが存在しない場合が存在するから。

function changeBackgroundColor<T>(element: T) {
  // any型だとコンパイルエラーは回避できる
  // 型チェックされないためバグの可能性
  (element as any).style.backgroundColor = 'red'
  return element
}

extends キーワードを利用すると、ジェネリクス型Tを特定の型に限定することができる。
今回の例だと以下の様に記述する。

function changeBackgroundColor<T extends HTMLElement>(element: T) {
  element.style.backgroundColor = 'red'
  return element
}

extendsキーワードはインターフェースに対しても使う。

interface ValueObject<T> {
  value: T;
  toString(): string;
}

class UserId implements ValueObject<number> {
  publick value: number;

  publick constructor(value: number) {
    this.value = value
  }

  public toString(): string {
    return `${this.value}`
  }
}

class Entity<ID extends ValueObject<unknown>> {
  private id: ID;

  public constructor(id: ID) {
    this.id = id;
  }
}

上記のEntityクラスはValueObjectインターフェースを実装しているクラスをIDとして受ける形。
型引数の制約はimplementsではなくextends

Conditional Types

要約:型の条件分岐。 T extends U ? X : Y と記述する。

type IsString<T> = T extends string ? true : false;
const a: IsString<'a'> = true

extendsの使い方を理解した。
Ifも解けた。

type If<C extends boolean, T, F> = C extends true ? T : F;
フロントエンドえんじにゃーフロントエンドえんじにゃー

Pick

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

もう一度 Pickに挑戦。
Mapped Typesを理解する。

Mapped Types

要約:主にユニオン型とinを組み合わせて、キー部分を定義したオブジェクトの型

type Language = 'en' | 'fr' | 'it' | 'es'

type Butterfly = {
  [key in Language]: string;
}

consut butterfly: Butterfly = {
  en: "Butterfly",
  fr: "Papillon",
  it: "Farfalla",
  es: "Mariposa",
}

Mapped Typesにはプロパティが追加できない

type Language = 'en' | 'fr' | 'it' | 'es'

type Butterfly = {
  [key in Language]: string;

  // ↓エラー
  name: string
}

上記の様なオブジェクト型を定義したい場合は、別々で定義し、インターセクション型で定義する

type KeyValues = {
  [key in Language]: string;
}
type Name = {
  name: string
}
type KeyValuesAndName = KeyValues & Name

TypeScriptでTruthyを書く場合、予めジェネリクスの入力側(?)をextendsで絞る

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

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

typescript @ts-expect-error ってなに?

直後の行が型エラーの場合、その型エラーをなくす
つまり、正常な行の直前に@ts-expect-errorを記述すると、逆に型エラーが表示される

Mapped Typesと型制約をつかえばいいはずだが、わからん。

https://dev.classmethod.jp/articles/type-challenges-easy-try/

を参考にすると、以下の記事が紹介されているので読む。
https://qiita.com/ryo2132/items/ce9e13899e45dcfaff9b#mapped-type-での利用

なるほど自力で解けた。

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

考え方は、

  • ジェネクスKに型制約をつける
  • Kはunion型だから、それをMapped Typesで書ける
  • keyにはunion型をバラした値が入ってくるからそれを利用してT[key]でTで指定されている型が取得できる
フロントエンドえんじにゃーフロントエンドえんじにゃー

Readonly

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

また詰まった。
ユーティリティ型のReadOnlyはObject.freeze的な動きをする。
のは分かる。

それをTSでどう表現するか

https://qiita.com/wataru86/items/f1356b5d1aef30012a20

プロパティに対してreadonly修飾子をつけることで、readonlyにできるよう。

let obj: {
  readonly foo: number;
};

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

これとMapped Typesを合わせればよかった。

フロントエンドえんじにゃーフロントエンドえんじにゃー

Tuple to Object

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

https://qiita.com/momosetkn/items/0874049f8a2af31c26cd

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

Mapped Typesでkeyof, typeofを使う方向だとは思う。

typeofはconstで定義された実体の値を型化するものなので、この場合は違う気がしてきた。

array型は、T[number]と記述することで値を型として抽出できる。裏挙動すぎる。
あとはMapped Typesを使う。

解説があった
https://zenn.dev/luvmini511/articles/d89b3ad241e544

index signatureというものがあり、indexを指定することで、型を直接取得することができる。

type person = {
  'name': string
  'age': 10
}

type a = person['name'] // string
type b = person['age'] // 10 (numberではない) 

同じ感覚で、型そのものを指定すると、それに一致する形を丸ごと取得できる。
T[number]としていすれば、array型のindexはnumber型のため、
number型に一致するものをunion型として取得できる。

numeric index signatureと呼ぶ。

TypeScript のグローバル型です。string、symbol、または のいずれかになりますnumber。
https://www.totaltypescript.com/concepts/propertykey-type

フロントエンドえんじにゃーフロントエンドえんじにゃー

First of Array

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

index signatureを利用するのは分かってる。

↓これをどう通過するか。
Expect<Equal<First<[]>, never>>,

こんなの書いたけどとおらない。

type First<T extends ((number | object | undefined)[] | never)> = T extends never ? never : T[0] 

違う。neverは空配列の場合の型だ。
だからジェネリクス制約に書かなくても自然と発生するんだ。

いけた

type First<T extends ((number | object | undefined)[])> = T extends never[] ? never : T[0] 

never型を理解する

https://typescriptbook.jp/reference/statements/never

never型は「値を持たない」を意味する。

特性

  • never型の変数へは何も代入できない
  • ただし、never型の値は代入できる(無理やりasでアサーションして)
  • 逆にnever型の値はどんな型の変数にでも代入できる
  • 例外の場合の戻り値の型もnever
  • type NumberString = number & string; はneverになる。インターセクション型は作れない。
  • void型との違いは、void型はundefinedが代入できるが、neverはできない。
  • 関数の戻り値にneverを指定するとエラーになる

網羅性チェックに応用ができる。
defaultを入れるとTSが代入エラーを警告するようになる。

function printLang(ext: Extension): void {
  switch (ext) {
    case "js":
      console.log("JavaScript");
      break;
    case "ts":
      console.log("TypeScript");
      break;
    default:
     // Type 'string' is not assignable to type 'never'.
      const exhaustivenessCheck: never = ext;
      break;
  }
}

みんなこうやってた。。
ジェネリクス制約は毎回書かなくていいのか。なるほど。
あとinferを使ってるコードもあった。

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

あとinferを使ってるコードもあった。

type First<T extends any[]> = T extends [infer P, ...any[]] ? P : never

せっかくだから調べてみよう。

https://typescriptbook.jp/reference/type-reuse/infer
https://qiita.com/ehika/items/8f41d4a3c8f9df4af9c3
https://zenn.dev/brachio_takumi/articles/464106a6a80eca8ab919

Conditional Types(extendsを使った三項演算子のようなもの)の右辺でのみ利用できる演算子らしい。

Widening Literal Types, NonWodening Literal Types初めて知った。

const hoge = "HOGE";
// type hoge: "HOGE"
let sameHoge = hoge;
// type sameHoge: string

as const (constアサーション)すると、変数を別の変数に代入してもNonWidening Literal Typesのままで利用ができる。

infer
直訳すると、「推論」
型を割り出すことができる。

↓Tがidというプロパティを持っている場合、そのidの型を返し、なければneverを返す

type Id<T> = T extends { id: infer U } ? U : never 

上だと、わかりにくいと感じたのでもう少し掘り下げ。

下記の場合、Tが[K in keyof T](ユニオンをMappedしたもの)プロパティを持っているなら、その型を返すなければneverということ。

export type Unpacked<T> = T extends { [K in keyof T]: infer U } ? U : never;

なるほど、元々の問題を見直してみると、以下のように書いていたけど、
推論が怪しく感じる。

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

こっちのほうがより広い条件に当てはまった書き方に思える。

type First<T extends any[]> = T extends [infer P, ...any[]] ? P : never

型定義のスプレッドに慣れていないので一応見る。
https://zenn.dev/cookiegg/articles/typescript-spread-type

↓このユニオンarray型だと型ガードしにくい
type SpecificArray = (string|number)[];

明確にこう定義したほうが型ガードしやすいが汎用性がまったくない
type SpecificArray = [string, number, number]

上記は、↓で書くと先頭がstringであとがnumberといった書き方が実現できる。
type ArrayType = [string, ...number[]];

改めて以下を見ると、問題の内容が0番目さへ推論できればよいのだから、こういう書き方になる意味がわかった。

type First<T extends any[]> = T extends [infer P, ...any[]] ? P : never
フロントエンドえんじにゃーフロントエンドえんじにゃー

Exclude

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

  type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

type MyExclude<T, U> = any

mapped type, conditional type, typeof, inferの複合に見える

ロジック整理
Tはユニオンなので、そのままループしinの左辺KとUを比較する

むずい。ヒントだけあやかる。

Conditional Typesでいけそうだけど、Distributiveという概念をしらないといかんらしい。

https://zenn.dev/hrkmtsmt/articles/be9a20fa7d3aaf

ここで言う「分配」はユニオン型の事を指している

ユニオン型の場合の Conditinal Typesは自動でループぽい振る舞いになるみたい。

T extends U ? X : YのTの型引数にA | B | Cが渡された場合T extends U ? X : Yは(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)のように展開されます。

ほー

これはConditional Typesの挙動を理解していないと解けない。

type MyExclude<T, U> = T extends U ? never : T 
フロントエンドえんじにゃーフロントエンドえんじにゃー

Awaited

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

Promiseライクな型が内包する型の取得

これも文法の話。

https://zenn.dev/estra/articles/ts-with-promise-type-annotation

type X = Promise<string>
MyAwaited<X> // string

type MyAwaited<T> = Promise<U>

上記を観察
Promise型にジェネリクスでstringが渡されている
Primiseに渡されている

型が型を内包しているように見える

Promise オブジェクトの型はジェネリクスを使って Promise<Type> というような形式
Promiseという関数がstringを返す形になってる

Promise型をもう少し理解

https://typescriptbook.jp/reference/asynchronous/promise

内部挙動の話が多い

多分文法の話なので、ヒントがないと何とも

やっぱinferらしい。

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

こう書いてみると、プリミティブ型はokっぽい

↓Promiseの入れ子がさばけてない

Promise<Promise<string | number>>

がっつり解説を見よう。
解説みたけど、Promiseの入れ子の解決が書いていない。

再帰的にinferどうするか
他の解答を見て参考にしてみる。

なるほど、再度inferしたあとにconditional typeを挟んで、PromiseLikeに当てはまってたら、自身の型定義を指定すればいいのか

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

再帰まで気づいてるんだから、自分で自分を指定するところまで考えが及べばよかった。

type ◯◯の部分は関数のように再帰的に指定ができる学び

フロントエンドえんじにゃーフロントエンドえんじにゃー

Concat

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

type Result = Concat<[1], [2]>; // expected to be [1, 2]

型そのものが[1, 2]

一旦違うのは分かってるけど、以下の様な感じを考えた
numeric index signature

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

解説では、Variadic Tuple Typesを学習する必要があるとのこと
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#variadic-tuple-types

array型も普通の配列と同じ感覚でスプレッド演算子が使えるんだった。

type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U]
フロントエンドえんじにゃーフロントエンドえんじにゃー

Includes

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

type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`

必要そうな知識
Conditional Type
配列の値取得の為のnumeric index signature

雰囲気こんなん

 T extends U ? true : false

課題Ifに近い要素がある
配列の値をループする → numeric index signatureでユニオン型にする
ユニオンをmapped types S in T[number]する感じ

多分、裏仕様がありそう

ヒントにあやかる

思ったよりも必要な知識が多かった

Variadic Tuple Types
infer
Conditional Types
Recursive Conditional Types

Variadic Tuple Typesは昨日やったarray型をスプレッド演算子展開するやつ

Recursive Conditional Typesもやったことある気がする
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#recursive-conditional-types

再帰か、これ正式にリカーシブコンディショナルタイプっていうのか

Variadic Tuple Typesはもうちょい深める
https://qiita.com/uhyo/items/7e31bbd93a80ce9cec84

タプル型の中に...Tと書ける機能
あるタプル型の要素たちを別のタプル型に埋め込むことができます。

解説1行目を見た。
Conditional Typeがまだ直感的に理解できていないのかも。
A extends B はあくまでAが主体
U extends T[number] はinの中でifをかけていくイメージ

type Includes<T extends readonly any[], U> = U extends T[number] ? true : false;

falseとなるパターンでtrueとなってしまっているパターンがある。

自力で解けそうにないから解答を見たらわからんかった

type Includes<T extends readonly any[], U> = T extends [infer L, ...infer R]
  ? [U, L] extends [L, U]
    ? true
    : Includes<R, U>
  : false

日本語化する
Tがreadolyのタプル型
チェック対象がU
Tの先頭がL、それ以外がRと推論できて、
かつ先頭Lと末尾Uのリテラルarray型を作って、
先頭と末尾をひっくり返しても同じならtrue
そうでないなら再帰で再度Includesを実行
そのときにRをジェネリクスの第一引数にしている為、先頭が省かれたタプル型になる
Uはそのまま継続
あとは同じ条件で末尾まで行く

[infer L, ...infer R]が満たせないつまり、要素数が1になったらfalse

むず

type Includes<T extends readonly any[], U> =
  T extends [infer L, ...infer R] ?
    [L, U] extends [U, L] ?
      true : Includes<R, U> :
    false;

が無理だったので、回答例を見てたら独自のEqualジェネリクス型を組んでチェックしてる人がいた。

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

true extends { mapped } ができるんか

右辺でtypeに代入してるはずなのに唐突にアロー関数が出てきて頭がバグってる

(<T>() => T extends X ? 1 : 2 ) extends (<T>() => T extends Y ? 1 : 2) ? true : false

ChatGPTに聞いてみたらかなり高度な型定義らしい

高階型(Higher-Order Types)
<T>() => T extends X ? 1 : 2 は型 T を引数として受け取り、T が X に割り当て可能な場合は 1 を、そうでない場合は 2 を返す関数の型を表します。この高階型は、型の割り当て可能性を評価するために使われます。

ジェネリック型の拡張性(Generics and Extensibility)
<T>() => T extends X ? 1 : 2 のような型を使うことで、任意の型 T が X に対してどのように振る舞うかをテストできます。これを Y にも同様に適用し、これら二つの関数型が等価であるかどうかをチェックします。

やりたいことは、
type MyEqual<X, Y> = (<T>() => T extends X ? 1 : 2 ) extends (<T>() => T extends Y ? 1 : 2) ? true : false

のXとYが等価かどうかの判定らしい

フロントエンドえんじにゃーフロントエンドえんじにゃー

Parameters

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

ついにeasy最終課題

arrayの引数をmappedで使うんだと思う。
引数に対するアプローチがわからん。

引数の型だけを抜き出せばいいからmappedとも少しちがう気がする。

function foo(arg1: string, arg2: number): void {}
// ↓
[string, number]

引数に対する構文の知識がいる

雰囲気以下のような感じ。
引数部分をinferとして

type MyParameters<T extends (...args: any[]) => any> = T extends infer U => any ? U[number] : []; 

関数のinferの書き方を学ぶ必要があるとのこと

少し修正したらベースの書き方はいけた。

type MyParameters<T extends (...args: any[]) => any> =
  T extends (args: infer U) => any ? U : []; 

あとは、Uを加工する。
少し近づいてる気がする。
inferで取得したUをconditional typesでタプル型に絞り込んで、
numeric index signatureを書けば取得できると思ったら違うらしい。

type MyParameters<T extends (...args: any[]) => any> =
  T extends (args: infer U) => any ?
    U extends any[]
      ? U[number]
      : []
    : []

解答を見たら、ちょっと思っていたこととずれていた

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

関数の型を再現する場合、引数のスプレッド演算子の部分も忠実に書かないといけない
ジェネリクス制約が T extends (...args: any[]) => any こう書かれていたら、
inferで書く際もT extends (...args: infer U) => any と書かないといけない。

引数はデフォルトでタプル型になっているため、関数の型は忠実に書くという知識だけ知っていれば、conditional typesで取得するまではできた。

フロントエンドえんじにゃーフロントエンドえんじにゃー

easy復習

Exclude

Distributive Conditional Types がまだ理解できていない。

type ConditionalTypes<T, U> = T extends U ? X : Y;
// Tが a | b となるのが、Distributive Conditional Types

Distributiveは分配という意味
'a' | 'b' extends U ? X : Y の場合、'a' extends U ? X : Y'b' extends U ? X : Y が自動で展開される。

上記を利用して、以下のようにConditional Typesを書くと、当てはまらない型の場合にneverとなるため、
ユーティリティ型のExcludeが再現できる。

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

Awaited

なんとなくinfer と 再帰条件型は覚えてた。
再帰で型を特定する場合、都度ジェネリクス制約を挟む必要がある。

↓こう書いた時、

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

↓このパターンのテストが満たせていない

type T = { then: (onfulfilled: (arg: number) => any) => any }

Expect<Equal<MyAwaited<T>, number>>

PromiseLike<T>インターフェイスがビルトインで存在するとのこと
https://qiita.com/suin/items/b9d00dff380486338ecd

↓これでいけた

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

If

Conditional Types(条件型)

Concat

Variadic Tuple Types(可変長タプル型)

Includes

クソむずかった記憶

inferを直感的に利用できないときに配列を分解する方法に慣れてない。

ジェネリクスを分解する際にとりあえず T extends [inferU] ? (ここで条件型をかます) を覚えておく。

↓この形は理解

type Includes<T extends readonly any[], U> =
  T extends [infer L,  ...infer R]
    ? [L, U] extends [U, L]
      ? true : Includes<R, U>
    : false

↓このテストが満たせていない

 Expect<Equal<Includes<[{ a: 'A' }], { readonly a: 'A' }>, false>>,
  Expect<Equal<Includes<[{ readonly a: 'A' }], { a: 'A' }>, false>>,

type-challenges側で用意されたUtilを使ってる人もいたが本質ではない。

type Includes<T extends readonly any[], U> =
  T extends [infer L,  ...infer R]
    ? Equal<U, L> extends true
      ? true : Includes<R, U>
    : false

これっぽいのを利用してる人が多い

type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
  ? 1
  : 2
  ? true
  : false;

Push

Unshift

どちらも Variadic Tuple Type

Parameters

inferで解決
関数の場合は、しっかり全部書く

T extends (...args: infer U) => any ? U : []
フロントエンドえんじにゃーフロントエンドえんじにゃー

Omit

https://github.com/type-challenges/type-challenges/blob/main/questions/00003-medium-omit/README.ja.md

Conditional Typeの分配を使うと思ってる。
これまでと違うのはTがarrayではなくオブジェクト

ちがう。mapped・・・の逆だ

mapped → index → 存在しないやつをinferで作る?

正解ではないけど、文法的には書ける。

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

ヒントを見る

Key Remapping in Mapped Typesという知識が必要らしい
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#key-remapping-in-mapped-types

Mapped Typesを利用して元のプロパティから新しいプロパティを生成したり、あるプロパティを除外する為にはas句を使用する
https://zenn.dev/link/comments/618ef3139c93c0

Capitalizeというビルトインの型がTS4.1からあるらしい
そして、その型定義でinstrinsicというワードが出てきた。

type Capitalize<S extends string> = intrinsic;

全部書いてた
https://zenn.dev/uhyo/articles/typescript-intrinsic

これは実装がコンパイラの内部実装として隠蔽されていることを意味しています。言い換えれば、TypeScriptの通常の型定義では表現できないような特殊な機能を表現するものです。
intrinsicキーワードの必要性
TypeScript 4.1では文字列の大文字・小文字変換の機能を実装することになりましたが、実は当初の実装ではintrinsicを使っていませんでした。代わりに、次のようにuppercase・lowercase・capitalize・uncapitalizeという4つのキーワードによる新たな構文を定義され、それらを用いる方式となっていました

話を戻してas句の2つの使い方

  • template literal types(リテラル型 + テンプレートリテラル)と組み合わせて利用するリネーム
  • as句の中でneverを返した場合に、プロパティ除外ができる

今回の課題は2つ目の使い方

課題に関して

type MyOmit<T, K extends keyof T> = {
  [key in keyof T as key extends K ? never : key]: T[key]
}

keyがユニオン型に含まれているかどうかをConditional Typesで判断

key extends K ? never : key

T as ◯ で◯がneverなら、for文ないでcontinueされるイメージ

他の解答でasを使わない別解があった。
カスタムでExclude型を作成し、Mappedを行っている。
as句でやっているイメージはこれ

type MyExclude<A, B> = A extends B ? never : A;
type MyOmit<T, K extends keyof T> = { [S in MyExclude<keyof T, K>]: T[S] }
フロントエンドえんじにゃーフロントエンドえんじにゃー

Readonly 2

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

困りごと
2 argument typeのときの絞り方
readonlyの場合分けの書き方

ヒントを見た

インターセクション型を使うらしい(&で繋ぐやつ)
https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types

イメージ
asを利用して、一致する場合、一致しない場合を書いてインターセクション型(&)でガッチャンコ

大枠はこれでいけた

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

あとは2 argument typeのときの絞り方
答えを見たら、オプション引数(?)的なので絞ると思っていたら、初期値を代入する形で対応していた。

ジェネリクスでも=を使った代入が利用できる。

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

解答を見たらこっちのほうがシンプルだった。
一旦Kに該当するものすべてにreadonlyを付与し、それ以外は通常の型にするイメージ

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]
}
フロントエンドえんじにゃーフロントエンドえんじにゃー

Deep Readonly

https://github.com/type-challenges/type-challenges/blob/main/questions/00009-medium-deep-readonly/README.ja.md

Mapped Typesを2段階でかける。

Mapped利用メモ
オブジェクト → keyof
ユニオン → そのまま

Mappedの場合もユニオン型には勝手に分配が適用される

なんとなくこんな雰囲気のやつを考えた。
オブジェクトの入れ子になってる部分が課題

type DeepReadonly<T> = {
  readonly [key in keyof T]: T[key] extends PropertyKey ? T[key]: DeepReadonly<T>
}

ヒントを見る
必要な知識は以下。大体以下は考慮できていた。
Mapped Types
Indexed Access Types
Conditional Types
Recursive Conditional Types

つまり T[P] がオブジェクトならさらにサブオブジェクトまで readonly とし、それ以外ならそのまま T[P] を返せばよい

これも把握している。ただ、オブジェクトならの条件の書き方が分からない。
PropertyKeyではないみたい。

またでたオブジェクトの判定は Record というビルトイン型を利用する
https://typescriptbook.jp/reference/type-reuse/utility-types/record

Record<Keys, Type> → プロパティがKeys(Keysはユニオン型)で、その値の型がTypeのオブジェクトを作るユーティリティ型

値がオブジェクト型かどうかは Record<string, unknown> で判定する

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends Record<string, unknown> ? DeepReadonly<T[P]> : T[P];
}

ただし、これでもエラーになっている。
解答を見てみる。
extends Function でFunction型の判定条件を追加している。
というか Record<string, unknown> の知識がなくても { [k in string]: any } でいいんだ。

そもそもRecordやオブジェクト判定を利用しない人がいた。
keyofがneverならオブジェクト以外という考え方(最初のProparyKeyの考え方に近い)

type DeepReadonly<T> = {
  readonly [P in keyof T]: keyof T[P] extends never ? T[P] : DeepReadonly<T[P]>
}

こっちと何が違うか

  readonly [P in keyof T]: T[P] extends Record<string, unknown> ? DeepReadonly<T[P]> : T[P]

これだと、以下の部分の展開がうまくできていない。

      l: [
        'hi',
        {
          m: ['hey']
        },
      ]

keyofをカマスと自然と配列の展開が行われる

フロントエンドえんじにゃーフロントエンドえんじにゃー

Tuple to Union

https://github.com/type-challenges/type-challenges/blob/main/questions/00010-medium-tuple-to-union/README.ja.md

パット見簡単に見える。

type TupleToUnion<T extends any[]> = T[number]

やっぱりインデックス型で一発だった。

他の回答例

ジェネリクス制約を利用せず、Conditional Typeでinferを利用している

export type TupleToUnion<T> = T extends Array<infer ITEMS> ? ITEMS : never

ArrayLike型を使っている。
なぜかF、lastの形でinferを使っている

type TupleToUnion<T extends ArrayLike<any>> = T extends [infer F, ...infer Last] ? TupleToUnion<Last> | F : never