はじめに
このシーズンの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]
}
回答のアプローチ
- 汎用パラメータの定義:
MyPicK
には2つの汎用パラメータ、T
とK
が必要とします。ここで、T
はオブジェクトタイプで表し、K
はTの属性キーのユニオン型となります。extends
を使用して、K
がT
のキーのサブセットであることを保証します。 - マッピング処理:マッピング型を使用して
K
内の各キーに対して繰り返し処理を行います。マッピング型の構文は[P in K]
で、ここでP
はK
のユニオン型に含まれる各プロパティキーを表します。 - インデックスアクセス型:
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を実現するためには、以下の二つのステップです:
- 引数
K
はオブジェクトT
のキーに存在するかをチェックします-
K
がT
のキーと互換性があるかどうかをextends
を使用してチェックします。 -
K = keyof T
というデフォルト型指定は、K
が特に指定されていない場合にT
の全てのキーをデフォルト値として割り当てるために使われます。
-
- 読み取り専用にするプロパティとするしないプロパティを区別します
-
P in keyof T as P extends K ? P : never
T
とは、T
のキーP
を調べて、P
がK
に含まれていれば、それを読み取り専用にするキーとして扱います。含まれていなけてば、何もしません。 -
&
を使用して、読み取り専用にしないプロパティを組み合わせる際に、先ほどの判定を反転されたロジックを使います。
-
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
は、オブジェクト内の全てのプロパティを再起的に読み取り専用にするための方です。
- マッピング処理:
Readonly
と同様に、T
の各キーに対していマッピングを行い、それらを読み取り専用にします。 - 条件型と関数のチェック:
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
解答アプローチ
- 分析した例によれば、入力型は
Promise
だけじゃなくて、then
を含むPromise
に似た型も考慮に入れるべきであるため、入力型はT extends PromiseLike<any>
を使用することが適切です。 -
PromiseLike
の内部内容の型を推定することが必要ですので、infer P
を使います。 - 推定型
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
型は、ある型が配列にあるかどうかチェックする方法です。
- 条件型:指定した型
T
は配列T
の中で存在かどうかをチェックすると、条件型を使います。 - 配列分割:条件型おいて
T
が[infer F, ...infer R]
の形状にマッチするかどうかでタプルを分解します。ここで、F
はタプルの最初のヘッドで、infer R
はそれ以外の要素を意味します。 -
Equal
:現在調べて要素F
とU
を比較するために、Equal<F, U>
を使います。もしそれがtrue
を拡張する場合、マッチが見つかったということで、最終にtrue
を返します。 - 再帰:マッチ見つかれない場合は、タプルの残り部分は同じチェックを繰り返します。リストの次のアイテムをチェックして続け、マッチが見つかるか全てのアイテムを検索したまで続けます。
- ベースケース:もし
T
が[infer F, infer R]
の構造とマッチしない場合、検索待ちアイテムがなくなったことを意味します。つまり全部の要素をチェックし終わったら、U
がタプル内部で見つからなかったので、false
を返します。