📆

Record型やReturnType型などのユーティリティ型をみてみる

2023/12/24に公開

ユーティリティ型

TypeScriptには標準で組み込まれている型として、ユーティリティ型があります。
代表的なものにReadonly型やPartial型などがあり、下の記事で実装を確認しましたが、ほかのユーティリティ型についてもみてみたいと思います。
https://zenn.dev/axoloto210/articles/advent-calender-2023-day21

Mapped Typesを簡単に使えるRecord

Record<K, T>型は、Kにプロパティキーのユニオン型、Tにプロパティの値の型を指定することで、対応するオブジェクト型を取得できます。

type PropertyKeys = "octopus" | "squid"

type Obj = Record<PropertyKeys, string>
// type Obj = {
//     octopus: string;
//     squid: string;
// }

Record<K, T>型の実装は以下のようになっています。

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

K extends keyof anyから、Kに指定できるのはプロパティキーとして取りうる型、つまりstring | number | sympolの部分型に限定されています。

type anyKey = keyof any
//type anyKey = string | number | symbol

実装からRecord型というのは、Mapped Typesという少し複雑な機能をユーティリティ型としてその複雑さを隠蔽し、使いやすくした型であることがわかります。
https://zenn.dev/axoloto210/scraps/2caa329b85f519#comment-17a2c9b5e144d9

https://zenn.dev/axoloto210/articles/advent-calender-2023-day22

Promiseを外すAwaited

Awaited<T>型は、非同期処理のawaitに倣って作られた型で、再帰的にPromiseを外した型を返します。

async function asyncFunc() {
    setTimeout(()=>{console.log("Loading...")}, 3000)
    return "success"
}

async function main(){
const promiseStr = asyncFunc()
//const promiseStr: Promise<string>
console.log(promiseStr)

const str = await asyncFunc()
//const str: string
console.log(str)

}

const promiseStr = asyncFunc()
//const promiseStr: Promise<string>
type Str = Awaited<typeof promiseStr> 
//type Str = string

main()

//---Logs---
//Promise: {} 
//"success" 
//"Loading..." 
//"Loading..." 
//"Loading..." 

asyncによる非同期関数の返り値はPromiseでラップされた型が返ってきます。awaitをつけると非同期処理の完了を待つため、返り値の型はPromiseが外れた状態となりますが、これと同じようにAwaited<T>型を使うことでPromiseを外すことができます。

Awaited<T>の実装は以下のようになっています。

/**
 * Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to `never`. This emulates the behavior of `await`.
 */
type Awaited<T> = T extends null | undefined ? T : // special case for `null | undefined` when not in `--strictNullChecks` mode
    T extends object & { then(onfulfilled: infer F, ...args: infer _): any; } ? // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
        F extends ((value: infer V, ...args: infer _) => any) ? // if the argument to `then` is callable, extracts the first argument
            Awaited<V> : // recursively unwrap the value
        never : // the argument to `then` was not callable
    T; // non-object or non-thenable

T extends null | undefined ? T :の部分により、Tとしてnullundefinedが渡された場合にはそのままTが返されます。また、T extends object & ... ? ... : Tの部分から、Tとしてオブジェクト以外が渡された場合にもそのまま返されることがわかります。
Tがオブジェクトで、{ then(onfulfilled: infer F, ...args: infer _): any; }の部分型であるなら、つまりthenメソッドを持ち、非同期処理成功時のコールバック関数onfullfilledを持っているならば、その関数をFとして次の条件判定に移ります(thenが呼び出せない場合などはnever型が返ります)。
F extends ((value: infer V, ...args: infer _) => any) ? Awaited<V> : never の部分では、onfullfilledで指定したコールバック関数がvalueを引数に持っている場合にはそのvalueの型Vに対してAwaited<V>を適用します。Awaitedの中にAwaited型を入れ込むことで再帰的にPromiseを外しているわけですね。

関数の引数の型、Parameters

Parameters<T>は関数の型Tを渡すことで、その関数の引数のタプル型を返します。

function numToString(num: number, msg:string){
    console.log(msg)
    return String(num)
}

type P = Parameters<typeof numToString>
//type P = [num: number, msg: string]

実装は以下のようになっています。

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

型引数Tは関数の型であるという制約がT extends (...args: any) => anyの箇所で課されており、T extends (...args: infer P) => any ? P : never;の箇所で引数の型Pinferによって取得し、そのまま返しています(inferはConditional Typesの中で使用できるジェネリクス型です)。

関数の返り値の型を返す、ReturnType

ReturnType<T>は関数の型Tから返り値の型を返すユーティリティ型です。

function numToObj(num: number){
    return {n:num, msg:"Hello", foo:true}
}

type R = ReturnType<typeof numToObj>
// type R = {
//     n: number;
//     msg: string;
//     foo: boolean;
// }

ReturnType<T>の実装はParameter<T>の実装とinferが引数型か返り値型かの違いしかなく、とても似たものになっています。

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

文字列リテラル型を操作できる組み込み文字列型

文字列リテラル型の大文字・小文字への変換ができる型も存在します。
それが、Uppercase<T>,Lowercase<T>,Capitalize<T>,Uncapitalize<T>というユーティリティ型です。

type literalStr = "OctoPus" | "sQuID"

type P = Uppercase<literalStr>
//type P = "OCTOPUS" | "SQUID"

type L = Lowercase<literalStr>
//type L = "octopus" | "squid"

type C = Capitalize<literalStr>
//type C = "OctoPus" | "SQuID"

type UC = Uncapitalize<literalStr>
//type UC = "sQuID" | "octoPus"

Uppercase<T>は全ての文字を大文字に、Lowercase<T>は全ての文字を小文字にします。
また、Capitalize<T>,Uncapitalize<T>はそれぞれ先頭の文字を大文字、小文字にします。
これらのユーティリティ型の実装は以下のようになっています。

/**
 * Convert string literal type to uppercase
 */
type Uppercase<S extends string> = intrinsic;

/**
 * Convert string literal type to lowercase
 */
type Lowercase<S extends string> = intrinsic;

/**
 * Convert first character of string literal type to uppercase
 */
type Capitalize<S extends string> = intrinsic;

/**
 * Convert first character of string literal type to lowercase
 */
type Uncapitalize<S extends string> = intrinsic;

intrinsicは型エイリアスがTypeScriptのコンパイラが提供する実装を参照することを表すもので、どのような実装がされているかはコンパイラを確認する必要があります。
https://github.com/microsoft/TypeScript/pull/40580

GitHubで編集を提案

Discussion