banner
raye~

Raye's Journey

且趁闲身未老,尽放我、些子疏狂。
medium
tg_channel
twitter
github
email
nintendo switch
playstation
steam_profiles

TSチャレンジ記録(1)

vipul-jha-a4X1cdC1QAc-unsplash

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 のオブジェクトのキーはstringnumbersymbolのいずれかでなければならないからです。ここをT extends readonly any[]に変更すると、undefinedbooleannullのような値がキーになり、エラーが発生する可能性があります。

もう一つ理解しにくいのは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;
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。