0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[TS]TypeChallengesからTypeScriptを復習しましょう_1

Posted at

はじめに

このシーズンのblogは自身がTypeScriptを再学習した記録です。Type Challengesを練習することで、TypeScriptの基礎知識を復習しながら補充します。
この記事の対象はTypeScript基礎知識は持っている初心者です。ゼロからの方は、TypeScriptの基本型、ユニオン型、InterfaceとTypeの相関概念を学習した後、この記事を読んでみてみてください。

4. Pick

// Challenge
import type { Equal, Expect } from './test-utils'
type cases = [
	Expect<Equal<Expected1, MyPick<Todo, 'title'>>>,
	Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>,
	// @ts-expect-error
	MyPick<Todo, 'title' | 'completed' | 'invalid'>,
]
interface Todo {
	title: string
	description: string
	completed: boolean
}
interface Expected1 {
	title: string
}
interface Expected2 {
	title: string
	completed: boolean
}

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

回答のアプローチ

  1. 汎用パラメータの定義:MyPicKには2つの汎用パラメータ、TKが必要とします。ここで、Tはオブジェクトタイプで表し、KはTの属性キーのユニオン型となります。extendsを使用して、KTのキーのサブセットであることを保証します。
  2. マッピング処理:マッピング型を使用してK内の各キーに対して繰り返し処理を行います。マッピング型の構文は[P in K]で、ここでPKのユニオン型に含まれる各プロパティキーを表します。
  3. インデックスアクセス型:T内の各プロパティキーPに対して、T[P]を使用して対応する形を取得します。

相関ポイント

  • extends:ジェネリック型や条件型などうで型の制約を定義する際に使用されます。extendsを使用することで、特定の型のサブセットかどうかを指定したり、ある型が他の型に代入可能かをチェックすることができます。
// ジェネリック型で使用例
function identity<T extends number>(arg: T):T {
	return arg
}

// 条件型で使用例
type IsString<T> = T extends string ? true : false

type IsStringCheck1 = IsString<string>; // true
type IsStringCheck2 = IsString<number>; // false
  • keyof:TypeScriptのキーワードで、オブジェクト型の全てのキーを取得するために使用されます。取得結果は、それらのキーを文字列リテラルのユニオン型として表します。
inerface Person {
	name: string;
	age: number;
	hasDriverLicense: boolean
}

type PersonKeys = keyof Person; // 'name' | 'age' | 'hasDriverLicense'

7.Readonly

// ============= Test Cases =============
type cases = [
	Expect<Equal<MyReadonly<Todo1>, Readonly<Todo1>>>,
]
interface Todo1 {
	title: string
	description: string
	completed: boolean
	meta: {
	author: string
	}
}
// ============= Answer =============
type MyReadonly<T> = {
	readonly [P in keyof T]: T[P]
}

解答アプローチ

取得したいのタイプは処理前とキー及び型は全て同じですが、readonly修飾子を使用してオブジェクトのプロパティを読み取り専用に変更します。これを実現するたには、マッピング処理を利用してreadonly修飾子各プロパティに追加します。

8.Readonly2

type cases = [
Expect<Alike<MyReadonly2<Todo1>, Readonly<Todo1>>>,
Expect<Alike<MyReadonly2<Todo1, 'title' | 'description'>, Expected>>,
Expect<Alike<MyReadonly2<Todo2, 'title' | 'description'>, Expected>>,
Expect<Alike<MyReadonly2<Todo2, 'description' >, Expected>>,
]
// @ts-expect-error
type error = MyReadonly2<Todo1, 'title' | 'invalid'>
interface Todo1 {
	title: string
	description?: string
	completed: boolean
}
interface Todo2 {
	readonly title: string
	description?: string
	completed: boolean
}
interface Expected {
	readonly title: string
	readonly description?: string
	completed: boolean
}
// ============= Answer =============
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]
}

解決アプローチ

Readonly2は、オブジェクトのプロパティを読み取り専用にする型ですが、上記のReadonlyとの違いは、オプションとしてプロパティのキーの部分集合Kを指定できる点です。指定したKのキーに対応するプロパティを読み取り専用にし、Kが指定されていない場合は、全てのプロパティを読み取り専用にします。

だから、Readonly2を実現するためには、以下の二つのステップです:

  1. 引数KはオブジェクトTのキーに存在するかをチェックします
    • KTのキーと互換性があるかどうかを extendsを使用してチェックします。
    • K = keyof T というデフォルト型指定は、Kが特に指定されていない場合にTの全てのキーをデフォルト値として割り当てるために使われます。
  2. 読み取り専用にするプロパティとするしないプロパティを区別します
    • P in keyof T as P extends K ? P : never Tとは、TのキーPを調べて、PKに含まれていれば、それを読み取り専用にするキーとして扱います。含まれていなけてば、何もしません。
    • &を使用して、読み取り専用にしないプロパティを組み合わせる際に、先ほどの判定を反転されたロジックを使います。

9.DeepReadonly

type cases = [
	Expect<Equal<DeepReadonly<X1>, Expected1>>,
	Expect<Equal<DeepReadonly<X2>, Expected2>>,
]
type X1 = {
	a: () => 22
	b: string
	c: {
		d: boolean
		e: {
			g: {
				h: {
					i: true
					j: 'string'
					}
				k: 'hello'
			}
			l: [
				'hi',
				{
					m: ['hey']
				},
			]
		}
	}
}
type X2 = { a: string } | { b: number }
type Expected1 = {
	readonly a: () => 22
	readonly b: string
	readonly c: {
	readonly d: boolean
		readonly e: {
			readonly g: {
				readonly h: {
					readonly i: true
					readonly j: 'string'
				}
				readonly k: 'hello'
			}
			readonly l: readonly [
				'hi',
				{
					readonly m: readonly ['hey']
				},
			]
		}
	}
}
type Expected2 = { readonly a: string } | { readonly b: number }
// ============= ANSWER =============
type DeepReadonly<T> = {
	readonly [P in keyof T]: T[P] extends Function ? T[P] : DeepReadonly<T[P]>
}

解答アプローチ

DeepReadonlyは、オブジェクト内の全てのプロパティを再起的に読み取り専用にするための方です。

  1. マッピング処理:Readonlyと同様に、Tの各キーに対していマッピングを行い、それらを読み取り専用にします。
  2. 条件型と関数のチェック:Tのプロパティは大きく分けて三種類あります:プリミティブ型、関数型とオブジェクト型です。プリミティブ型と関数型のプロパティには単純にreadonlyを適用するだけで済みです。しかし、オブジェクト型は、そのプロパティ自体だけではなく内部の全てのプロパティも読み取り専用にする必要があります。そのため、プロパティが関数でない場合は、T[P]を再帰的にDeepReadonlyを適用します。

189.Awaited

// ============= Test Cases =============
type X = Promise<string>
type Y = Promise<{ field: number }>
type Z = Promise<Promise<string | number>>
type Z1 = Promise<Promise<Promise<string | boolean>>>
type T = { then: (onfulfilled: (arg: number) => any) => any }

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>>,
]
// @ts-expect-error
type error = MyAwaited<number>

// ============= Answer =============
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer P> ?
P extends PromiseLike<any> ? MyAwaited<P> : P : never

解答アプローチ

  1. 分析した例によれば、入力型はPromiseだけじゃなくて、thenを含むPromiseに似た型も考慮に入れるべきであるため、入力型はT extends PromiseLike<any>を使用することが適切です。
  2. PromiseLikeの内部内容の型を推定することが必要ですので、infer P を使います。
  3. 推定型Pと内部構造がまだPromiseLike型の可能性があるかため、内部構造まだチェックして、同じPromiseLike型であれば、再帰的に処理を続けます。そして、PromiseLike型でない場合には、その型を返すようにします。

相関ポイント

  • PromiseLike:thenを持って、Promiseの基本的な行為に似たオブジェクトです。
let promiseLike: PromiseLike<number> = {
    then: () => {
	    return onfulfilled(42);
    }
};
promiseLike.then(value => console.log(value)); // 42
  • infer:条件型内で使用されるキーワードです。型をダイナミックに推論するために使います
type ExtractPromiseType<T> = T extends PromiseLike<infer U> ? U : never;
type ResolvedType = ExtractPromiseType<PromiseLike<string>>; // string

898.Includes

// ============= Test Cases =============
import type { Equal, Expect } from './test-utils'
type cases = [
Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Kars'>, true>>,
Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'>, false>>,
Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 7>, true>>,
Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 4>, false>>,
Expect<Equal<Includes<[1, 2, 3], 2>, true>>,
Expect<Equal<Includes<[1, 2, 3], 1>, true>>,
Expect<Equal<Includes<[{}], { a: 'A' }>, false>>,
Expect<Equal<Includes<[boolean, 2, 3, 5, 6, 7], false>, false>>,
Expect<Equal<Includes<[true, 2, 3, 5, 6, 7], boolean>, false>>,
Expect<Equal<Includes<[false, 2, 3, 5, 6, 7], false>, true>>,
Expect<Equal<Includes<[{ a: 'A' }], { readonly a: 'A' }>, false>>,
Expect<Equal<Includes<[{ readonly a: 'A' }], { a: 'A' }>, false>>,
Expect<Equal<Includes<[1], 1 | 2>, false>>,
Expect<Equal<Includes<[1 | 2], 1>, false>>,
Expect<Equal<Includes<[null], undefined>, false>>,
Expect<Equal<Includes<[undefined], null>, false>>,
]
// ============= Answer =============
type Includes<T extends readonly any[], U> = T extends [infer F, ...infer R] ?
	Equal<U, F> extends true ? 
		true 
		: Includes<R, U> 
	: false;

解答アプルーチ

Include型は、ある型が配列にあるかどうかチェックする方法です。

  1. 条件型:指定した型Tは配列Tの中で存在かどうかをチェックすると、条件型を使います。
  2. 配列分割:条件型おいてT[infer F, ...infer R]の形状にマッチするかどうかでタプルを分解します。ここで、Fはタプルの最初のヘッドで、infer Rはそれ以外の要素を意味します。
  3. Equal:現在調べて要素FUを比較するために、Equal<F, U>を使います。もしそれがtrueを拡張する場合、マッチが見つかったということで、最終にtrueを返します。
  4. 再帰:マッチ見つかれない場合は、タプルの残り部分は同じチェックを繰り返します。リストの次のアイテムをチェックして続け、マッチが見つかるか全てのアイテムを検索したまで続けます。
  5. ベースケース:もしT[infer F, infer R]の構造とマッチしない場合、検索待ちアイテムがなくなったことを意味します。つまり全部の要素をチェックし終わったら、Uがタプル内部で見つからなかったので、falseを返します。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?