TS チャレンジ記録(1)#
題目はここに記録されています: https://github.com/type-challenges/type-challenges/blob/main/README.md
このチャレンジは、挑戦者が TS の型システムをよりよく理解することを目的としており、type
という構文を通じて TS のさまざまな機能を実現することができ、なかなか面白いと感じています!
同時にtype
はチューリング完全なシステムですか?つまり、C++
のテンプレートのようなメタプログラミングを実現できるのでしょうか?🤔時間があればじっくり研究してみたいです:
https://github.com/microsoft/TypeScript/issues/14833
Hello World チャレンジ:
// 文字列であることが期待される
type HelloWorld = any
// これを機能させる必要があります
type test = Expect<Equal<HelloWorld, string>>
最初の例は主にtype
の使い方を示しています。答え:
type HelloWorld = string
既にこの問題の本質は、あなたに type の使い方を理解させるためのものであるため、まずは type の学習ノートを記録しておきます。そうしないと、後の問題が本当に理解できなくなります。
type の主な使い方#
ここで type の主な使い方をまとめておきます。後のコードが理解できなくなるのを防ぐためです(実際、私は最初に問題を解いてから type キーワードを逆に学んでいます😅)。
1. 基本型エイリアス#
type Word = string;
これは理解しやすいです。Word
という型をカスタマイズして、string
の代わりに使用します。
2. ユニオン型#
type StringOrNumber = string | number;
ここから複雑になってきますが、union
として理解できます。StringOrNumber
を通じて、string または number 型の変数を宣言できます。
ここでの型はリテラル型でもあり得ます。例えば:
type ABC = "A" | "B" | "C"
これは ABC 型の変数の値が"A","B","C"
のいずれかであることを示しています。
3. 交差型#
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
この交差型を使用する場合は、次のようになります。
let person: Person = {
name: 'Alice',
age: 30
};
4. typeof && keyof#
typeof
は変数または値の型を取得するもので、C++
の type に似ていますが、用途はより広範です。
例えば、関数にtypeof
演算子を使用すると、関数に対応する型を取得できます。これはC++
の auto 自動推論に似ています。
const foo = (arg1: string, arg2: number): void => {}
typeof
を使用して引数の型を定義します。
const bar = (f: typeof foo) => {
f("Hello", 42);
}
keyof
は型のすべてのキー名を取得します。以下の例を見てください。
type Person {
name: string;
age: number;
}
type K = keyof Person; // "name" | "age"
extends#
extends は、与えられたジェネリック変数が指定された型に代入できるかどうかを判断するために使用されます。
T extends U ? X : Y
もちろん、後で extends には多くの不思議な使い方があることに気づくでしょう🤣
in#
TS の in 演算子は、一般的な使い方の他に、型を遍歴するためにも使用できます。メタプログラミングとして使用できます。以下の例を見てください。
type Keys = 'a' | 'b'
type MyMap<T> = {
[K in Keys]: T
}
Keys は'a'
または'b'
のいずれかのリテラル型を表します。
K in Keys はこの 2 つの型を遍歴します。
最終的なMyMap
構造は次のように理解できます。
{
a: T, // 'a'型、実際にはkeyはaでなければなりません
b: T // 'b'型、実際にはkeyはbでなければなりません
}
infer#
infer の役割は、ある場所に置いて推論された型を指定することです(私の理解では)。
簡単な例を見てみましょう:
ここで(...args: any) => infer R
を使用します。これは関数の形式で、infer R
は全体として想像され、実際には矢印関数の戻り値を表します。したがって、infer R
は自然に戻り値の型になります。
type ReturnType<T> = T extends (...args: any) => infer R ? R : any;
使用例:ReturnType
が R をstring
型として推論したことがわかります。
type T = (a: number) => string
type R = ReturnType<T> // string
これらの知識を補完した後、問題を解き始めることができます😉
MyPick の実装#
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
keyof
を使用して型のすべてのプロパティからなるリテラルユニオン型を取得し、次に in で遍歴できます。
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
MyReadonly の実装#
interface Todo {
title: string
description: string
}
const todo: MyReadonly<Todo> = {
title: "Hey",
description: "foobar"
}
todo.title = "Hello" // エラー: readonlyプロパティを再割り当てできません
todo.description = "barFoo" // エラー: readonlyプロパティを再割り当てできません
プロパティにreadonly
を追加するだけです。
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
}
Tuple to Object#
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type result = TupleToObject<typeof tuple> // 期待される { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
このアイデアは難しくありませんが、すぐに答えを見ました。
type TupleToObject<T extends readonly (keyof any)[]> = {
[K in T[number]]: K;
};
いくつかの重要な部分を説明します:
keyof any
は、すべての表現可能な列挙型、すなわちstring | number | symbol
を表します。なぜなら、any
型はこの 3 つだからです🤣readonly (keyof any)[]
は、すべての読み取り専用の(string | number | symbol)[]
、すなわち混合型の配列を表します。[K in T[number]]
は、数字リテラルを使用して数字を表します。
ここでなぜ
readonly (keyof any)[]
を使用するのか?ではなくreadonly any[]
ではないのか?
なぜなら、最終的な結果はオブジェクトであり、JS のオブジェクトのキーはstring
、number
、symbol
のいずれかでなければならないからです。ここをT extends readonly any[]
に変更すると、undefined
、boolean
、null
のような値がキーになり、エラーが発生する可能性があります。
もう一つ理解しにくいのはT[number]
です。以下の例を見れば理解できます。
type Person = {
name: string;
age: number;
};
type PersonName = Person["name"]; // PersonNameは"string"型になります
type PersonAge = Person["age"]; // PersonAgeは"number"型になります
これは、T[K]
の構文を使用して対応する値の型を取得することを理解できます。
同様に:
type ArrayType = string[];
type ElementType = ArrayType[number]; // ElementTypeは"string"型になります
type TupleType = [string, number, boolean];
type TupleFirstElementType = TupleType[0]; // TupleFirstElementTypeは"string"型になります
type TupleSecondElementType = TupleType[1]; // TupleSecondElementTypeは"number"型になります
したがって、答えのT[number]
は配列内の各要素の型を表し、K in T[number]
を通じて各型を取得できます。
配列の最初の要素#
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type head1 = First<arr1> // 期待される'a'
type head2 = First<arr2> // 期待される3
この問題の考え方は、配列の最初の要素の型を要求しており、配列の要素の型は任意である(オブジェクトキーとしての制限はありません)。
最初の要素を直接取得する場合:
type First<T extends any[]> = T[0]
しかし、配列が空であるかどうかを判断する必要があります。2 つの判断方法があります。
最も簡単なのはもちろん:
type First<T extends any[]> = T extends [] ? never : T[0]
しかし、TS の型は値のように計算することもできます。たとえば、T['length']
を計算することもできます。したがって、
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]
もう一つの考え方は、配列を分解することですが、思いつきませんでした😓
type First<T extends any[]> = T extends [infer A, ...infer rest] ? A : never
タプルの長さ#
type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']
type teslaLength = Length<tesla> // 期待される4
type spaceXLength = Length<spaceX> // 期待される5
これは直接length
プロパティを取得するだけですが、readonly
でなくても構いません。元の配列に影響を与えないようにするために意図的に加えたのかもしれません。
type Length<T extends readonly any[]> = T['length']
Exclude#
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
これは最も理解しにくいかもしれません。答えはこれです:
type MyExclude<T, U> = T extends U ? never : T;
明らかにextends
は T が U 型に代入できるかどうかを判断するだけですが、
実際の実行プロセスは次のようになります。
'a' extends 'a' ? never : 'a'
、never を返します'b' extends 'a' ? never : 'b'
、'b'
を返します'c' extends 'a' ? never : 'c'
、'c'
を返します
最終的な結果を結合すると、自然に'b' | 'c'
が返されます。
この件については、以下の文書を参照してください。
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
Awaited#
これも非常に理解しにくいです🫠
type ExampleType = Promise<string>
type Result = MyAwaited<ExampleType> // string
最初は簡単そうに見えましたが、私は直接
type MyAwaited<T> = T extends Promise<infer U> ? U : never;
しかし、
Promise
の中にPromise
がネストされている可能性があることを考慮しましたか?
それでは改善します:(あなたは間違っていません、type は再帰をサポートします)
type MyAwaited<T extends Promise<any>> = T extends Promise<infer U>
? U extends Promise<any> ? MyAwaited<U> : U
: never;
理論的にはこれで十分だと思いますが、私は最も高評価の回答を見ました:
type Thenable<T> = {
then: (onfulfilled: (arg: T) => unknown) => unknown;
}
type MyAwaited<T extends Thenable<any> | Promise<any>> = T extends Promise<infer Inner>
? Inner extends Promise<any> ? MyAwaited<Inner> : Inner
: T extends Thenable<infer U> ? U : false
これには少し混乱しました。なぜThenable
型を追加し、さらにこの状況を判断する必要があるのでしょうか?まだ理解できていません🤔
Thenable
型をカスタマイズせずに、PromiseLike
を直接使用することもできますが、後で時間があれば再研究します。
If#
type A = If<true, 'a', 'b'> // 期待される'a'
type B = If<false, 'a', 'b'> // 期待される'b'
これは非常に簡単です。最初の値は必ずextends boolean
であり、true
型に代入できるかどうかを判断する必要があります(注意:直接C ? T :F
と書くことはできません🤣)。
type If<C extends boolean, T, F> = C extends true ? T : F;
Concat#
type Result = Concat<[1], [2]> // 期待される[1, 2]
驚くべきことに、配列型は...
演算子をサポートしています。
type Concat<T extends any[], U extends any[]> = [...T, ...U]
Includes#
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // 期待される`false`
ここではループを書く必要があるように見えますが、実際にはextends
に基づいて、ある型がユニオン型に変換できるかどうかを判断します。これにより、展開されて順に判断されます。
type Includes<T extends readonly any[], U> = U extends T[number] ? true : false
T[number]
は実際には次のようになります。
'Kars' | 'Esidisi' | 'Wamuu' | 'Santana'
そして、U extends T[number]
はそれぞれを判断します。
Push#
type Result = Push<[1, 2], '3'> // [1, 2, '3']
これは非常に簡単です。T を展開し、U を追加するだけです。
type Push<T extends any[], U> = [...T, U]
Unshift#
type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
非常に簡単なので詳しくは言いません。
type Unshift<T extends any[], U> = [U, ...T]
Parameters#
const foo = (arg1: string, arg2: number): void => {}
type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
これも非常に簡単です。関係を整理するだけです。
type MyParameters<T extends (...args:any) => any> = T extends (...args:infer P) => any ? P : never;