Closed22

Advent Calendar of TypeScript

うぱうぱ

12/1 オブジェクトの分割代入

オブジェクトの分割代入構文を用いると、オブジェクトの値を取り出す処理を比較的簡潔に書けます。

const object = {
  foo: 271,
  bar: "str",
  "1234": 314,
}
const { foo, bar } = object
console.log(foo) //271
console.log(bar) //str

変数名と同じプロパティ名に対応する値が変数に代入されます。
ただし、型注釈をつけることはできず、変数の型は型推論によって決定されます。

識別子でないプロパティ名

プロパティ名が識別子ではない場合(数字で始まっていたり、特定の記号を含んでいる場合)には、変数名を変更することで分割代入可能となります。

const object = {
  foo: 271,
  bar: "str",
  "1234": 314,
}
const { foo, "1234": var1234 } = object

console.log( foo ) // 123
console.log( var1234 ) // 314

ネストされているオブジェクト

ネストされているオブジェクトから値を取り出したいときにも活用できます。

const nestedObj = {
  foo: 123,
  obj: {
    bar: 1,
    obj2: {
      baz: 2,
    },
  },
}
const { obj: {bar, obj2: { baz }}} = nestedObj

console.log(bar)//1
console.log(baz)//2
うぱうぱ

12/2 配列の分割代入

配列についても分割代入が使用可能です。
オブジェクトの場合と同じで型注釈をつけることはできず、型推論のみによって型が決定されます。

const arr = ["hello", 123, true]

const [foo, bar] = arr

console.log(bar) //123

オブジェクトの分割代入と組み合わせることで、簡潔に値を取り出せます。

const arr = ["hello", 123, true]
const [foo, bar] = arr

const obj = { foo, arr }

const {
  arr: [x, y],
} = obj

console.log(x) //hello
console.log(y) //123

配列の中にオブジェクトが含まれている場合に、そのオブジェクトの一部だけを取り出すのに使うこともできます。

const arrayObj = [{ a: 1, b: 2 }, { c: 3 }]
const [{b},{c}] = arrayObj

console.log(b) //2
console.log(c) //3
うぱうぱ

12/5 インデックスシグネチャと型安全性

オブジェクトのプロパティ型の記法の一つにインデックスシグネチャというものがあります。
{[key名: string]: プロパティの型}のように記述することで、オブジェクトに含まれるプロパティの名前や個数が決まっていない場合に対応することができます。

type Obj = {
    [k: string]: number | string
}

const obj : Obj = {
    foo: 1,
    bar: 2,
    baz:'3'
}

型安全性

オブジェクトの型を動的に設定するのに便利な機能ですが、型安全性を損なう使い方ができてしまう点には注意する必要があります。具体的には、プロパティ名を動的に決めた場合、存在しないプロパティへのアクセス時にコンパイルエラーが発生しなくなってしまうというものです。

const str : string = 'foo'
const obj  = {
    one: 1,
    two: 2,
    [str]: 'test'
} 
//型は以下のように推論される
//const obj: {
//     [x: string]: string | number;
//     one: number;
//     two: number;
// }

console.log(obj.foo) // "test" 

console.log(obj) //{"one": 1, "two": 2, "foo": "test"} 
//存在しないプロパティにアクセスしても型エラーが出ない。
console.log(obj.notExistProp) //undefined

この例では、foo: stringという型のプロパティを設定しているはずなのに、任意のプロパティ名の存在を許してしまっています。実行時エラーにつながってしまう書き方もできるため、Mapを使用することが推奨されています。

うぱうぱ

Mapの型

Map

Mapはキーとそれに対応する値を保持する機能を持つオブジェクトです。
キーとして任意の値を用いることができ、オブジェクトをキーとすることも可能です。
また、Mapの型はMap<K, V>のように表されます(Kはキーの型、Vは値の型です)。

Mapの操作

Mapにキーと値を追加するにはsetメソッドを利用します。
また、Mapに格納された値を取得するgetメソッドや、引数として渡したキーがMapに保持されているかを判定できるhasメソッドなどもあります。

const map: Map<string|KeyObj,number> = new Map()
type KeyObj = {key: string}
const keyObj: KeyObj = {key: 'value'}
map.set('key1',1234)
map.set(keyObj,210)

console.log(map) //Map (2) {"key1" => 1234, {"key": "value"} => 210} 
console.log(map.has('key1')) //true
console.log(map.get('key1')) //1234

Mapの値を列挙する

Mapにはkeysvaluesentriesといった、保持している値を列挙するためのメソッドも備わっています。これらのメソッドの返り値はイテレータと呼ばれるもので、ループ処理での使用頻度が高いです。

const map: Map<string|KeyObj,number> = new Map()
type KeyObj = {key: string}
const keyObj: KeyObj = {key: 'value'}
map.set('key1',1234)
map.set(keyObj,210)

for(const key of map.keys()){
console.log(key)
}
//"key1" 
//{"key": "value"} 

for(const value of map.values()){
console.log(value)
}
//1234
//210

for(const[key, value] of map.entries()){
    console.log(key)
    console.log(value)
}
//"key1" 
//1234 
//{"key": "value"} 
//210 
うぱうぱ

12/7 WeakMapとガベージコレクション

WeakMap

Mapに似たオブジェクトにWeakMapというものが存在します。
Mapと違って列挙を行うためのメソッド(keysvaluesentries)を持たず、キーとしてはオブジェクト(もしくはnon-registered なシンボル)のみを設定することができ、数値や文字列を設定することはできません。

const weakMap = new WeakMap()
//Argument of type 'string' is not assignable to parameter of type 'object'.ts(2345)
weakMap.set('str','test')
//実行時エラーが発生(Invalid value used as weak map key)

ガベージコレクション

WeakMapMapと異なる点として、キーへの参照が弱参照である点があります。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management#garbage_collection

このことは、キーとして設定できる値がオブジェクトかnon-registered なシンボルしか許されていない理由の一端となっています。
registered なシンボルはガベージコレクションの対象とならないため、WeakMapのキーとしての使用が許可されていないようです。

Because registered symbols can be arbitrarily created anywhere, they behave almost exactly like the strings they wrap. Therefore, they are not guaranteed to be unique and are not garbage collectable. Therefore, registered symbols are disallowed in WeakMap, WeakSet, WeakRef, and FinalizationRegistry objects.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#shared_symbols_in_the_global_symbol_registry

Mapでキーとして使用されているオブジェクトは、Map自体がガベージコレクトされない限りはガベージコレクションの対象とならず、メモリ上に保持され続けます。これは、Mapがキーを列挙するメソッドを持っているため、キーとして登録されたオブジェクトが不要になったと判断されないためです。

WeakMapまとめ

キーを列挙するメソッドを持たないようにしたり、弱参照とならないキーを設定できないようにするなど、Mapの機能にいくつかの制約をつけることでメモリの節約ができるようにしたオブジェクトがWeakMapであると言えます。

うぱうぱ

12/8 readonly でプロパティを読み取り専用に

readonly

オブジェクトのプロパティを読み取り専用にする機能として、readonlyというものがあります。プロパティ名の前にreadonlyをつけることで、そのプロパティの値に代入しようとするとコンパイルエラーが発生するようになります。

const obj: {
    num: number
    readonly readonlyNum: number
} = {
    num:314,
    readonlyNum: 271
}

obj.num = 3141
obj.readonlyNum = 2718 // Cannot assign to 'readonlyNum' because it is a read-only property.(2540)

オブジェクトのプロパティ全体を読み取り専用にするReadonly

Readonly<T>という組み込みの型を使用すれば、オブジェクトのプロパティすべてを読み取り専用にできます。

type readonlyObj = Readonly<{
num: number
name: string
}>

const obj: readonlyObj = {
    num: 314,
    name: 'pi'
}

obj.num = 1 // Cannot assign to 'num' because it is a read-only property.(2540)
obj.name = 'one'// Cannot assign to 'name' because it is a read-only property.(2540)

Readonly<T>Tのプロパティをすべてreadonlyにしますが、プロパティがオブジェクトだった場合、そのオブジェクトに含まれるプロパティまではreadonly とならないので注意する必要があります(オブジェクト自体がreadonly となります)。

type readonlyObj = Readonly<{
  num: number
  numObj: {
    name: string
    num: number
  }
}>
const obj: readonlyObj = {
  num: 314,
  numObj: {
    name: "two",
    num: 2,
  },
}

obj.numObj.num = 3 //コンパイルエラーが発生しない
obj.numObj = {name: 'three', num: 3} //Cannot assign to 'numObj' because it is a read-only property.

as const

ネストの深い階層のプロパティも含めてreadonlyにしたい場合には、as constを使用する手もあります。
as constは、オブジェクトリテラルのプロパティを再帰的にreadonlyとします(型にas constをつけられるわけではない点に注意)。型アサーションとは異なり、型安全性を損なう機能ではないため、積極的に使用していけます。

const obj = {
    obj1: { obj2: { obj3: { num: 3, name: 'three' }}}
} as const

obj.obj1.obj2.obj3.name = 'four' // Cannot assign to 'name' because it is a read-only property.(2540)

型として使用したい場合には、as constで読み取り専用とした値からtypeofによって型を作成することができます。

const obj = {
    obj1: { obj2: { obj3: { num: 3, name: 'three' }}}
} as const

type NestedObj = typeof obj

NestedObj型は以下のようになります。

type NestedObj = {
    readonly obj1: {
        readonly obj2: {
            readonly obj3: {
                readonly num: 3;
                readonly name: "three";
            };
        };
    };
}
うぱうぱ

12/9 readonlyなプロパティは関数を通して変更されうる

オブジェクトをreadonlyなプロパティとしたときには、そのプロパティに値を代入しようとするとコンパイルエラーが発生するようになります。

type ReadonlyObj = {
    readonly num: number
}

const obj: ReadonlyObj = {
     num: 314
}

obj.num=271 //Cannot assign to 'num' because it is a read-only property.(2540)

しかし、以下のようにreadonlyではないプロパティを持つオブジェクト型を引数の型とする関数を経由して同様の処理を行うと、コンパイルエラーが起こることなくプロパティの値を変更できてしまうので注意が必要です。

type Obj = {
    num: number
}

type ReadonlyObj = {
    readonly num: number
}


const obj: ReadonlyObj = {
     num: 314
}

const changeNum = (obj: Obj)=>{
    return obj.num = 271
}

console.log(obj) //{ "num": 314 } 
changeNum(obj) // readonlyなプロパティを(コンパイルエラーなしで)書き換えられてしまう。
console.log(obj) //{ "num": 271 } 
うぱうぱ

12/12 ユーティリティ型(組み込み型)、PickとOmit

TypeScriptの標準ライブラリには、組み込み型、ユーティリティ型と呼ばれる汎用性の高い型が用意されています。以前に紹介したReadonly<T>型も組み込み型の一つです。
https://zenn.dev/link/comments/ecf10ccb3a921e

Pick<T, K>型とOmit<T, K>

Pick型は、オブジェクト型TからKで指定したキーをもつプロパティを取り出して新しいオブジェクト型を作り出す組み込み型です。KにはT型のキーをユニオン型で指定できます。

type T = { str: string, num: number, obj: { foo: string, bar: boolean } }

//type P = { num: number; str: string;}
type P = Pick<T,"num"|"str">
//type Q = { obj: { foo: string; bar: boolean;};}
type Q = Pick<T,"obj">
//type R = { foo: string; }
type R = Pick<T["obj"], "foo">

作成される型はオブジェクト型であることに注意が必要ですね。

Omit型はPick型とは反対に、Kで指定したキーをもつプロパティを除いたオブジェクト型を返します。

type T = { str: string, num: number, obj: { foo: string, bar: boolean } }

//type P = { obj: { foo: string; bar: boolean;};}
type P = Omit<T, "num" | "str">
//type Q = { num: number; str: string;}
type Q = Omit<T, "obj">
//type R = { bar: boolean;}
type R = Omit<T["obj"], "foo">

Pick<T, K>KにはK extends keyof Tという制約が課されており、Tのキーの部分型を指定できます。一方でOmit<T, K>Kに課されている制約はK extends keyof anyとなっており、PickのKよりも条件の緩い指定ができるようになっています。

うぱうぱ

12/13 組み込み型(Extract、Exclude、Partial、Required、NonNullable)

Extract<T, U>型、Exclude<T, U>

Extract<T, U>は型Tから型Uの部分型となる型を抽出する組み込み型です。抽出できないような型の指定がされた場合にはnever型が返されます。
Tとしてはユニオン型が指定されることが多いですが、必ずしもユニオン型である必要はありません。

type T = { str: string, num: number }
type U = T | number | boolean | "314"
//type P = "314"
type P = Extract<U, string>
//type Q = { str: string; num: number;}
type Q = Extract<T, {str: string}>
//type R = never
type R = Extract<U, {foo: boolean}>

Exclude<T, U>型は、Extract<T, U>とは反対に、型Tから型Uの部分型となる型を取り除く組み込み型です。すべての型情報が取り除かれるような型Uが指定された場合には、never型が返されます。

type T = { str: string, num: number }
type U = T | number | boolean | "314"
//type P = number | boolean | T
type P = Exclude<U, string>

//type Q = never
type Q = Exclude<T, {str: string}>

//type R = number | boolean | T | "314"
type R = Exclude<U, {foo: boolean}>

Partial<T>型とRequired<T>

Partial<T>型は、型Tのプロパティをオプショナルにする型です。

type T = { str: string, num: number }

// type P = {
//   str?: string | undefined
//   num?: number | undefined
// }
type P = Partial<T>

//type Q = number プリミティブな型はそのまま返ってきている。
type Q = Partial<number>

Readonly型と同様に、オブジェクトのプロパティを再帰的にオプショナルにするわけではない点に注意が必要です。

type NestedObj = { str: string, obj:{ str: string, num: number } }

//type R = {
//     str?: string | undefined;
//     obj?: {
//         str: string;
//         num: number;
//     } | undefined;
// }
type R = Partial<NestedObj>

Required型は、Partial型とは逆で、オプショナルな型をオプショナルではなくします。こちらも再帰的にオプショナル型ではなくすわけではないことに注意が必要です。

type T = { str?: string, num: number }

//type P = { str: string; num: number; }
type P = Required<T>
//type Q = number
type Q = Required<number>

type NestedObj = { str?: string, obj?:{ str?: string, num: number } }
// type R = {
//     str: string;
//     obj: {
//         str?: string;
//         num: number;
//     };
// }
type R = Required<NestedObj>

NonNullable<T>

NonNullable<T>型は型Tからnullundefinedを取り除いた型となります。オブジェクトのプロパティがnullの場合に取り除いたりはしません。

//type P = string
type P = NonNullable<string | null | undefined>

type T = null | {str: string, n: null}
//type Q = { str: string; n: null; }
type Q = NonNullable<T>

NonNullableの元の実装を見てみると、type NonNullable<T> = T & {};というようになっています。{}という型がnullもしくはundefined以外の部分型であることを利用して、nullundefinedを取り除いているわけですね。

うぱうぱ

12/14 オプショナルプロパティ"?:"と"undefinedとのユニオン型"は同じもの?

オプショナルプロパティとは、オブジェクト型の機能の一つで、プロパティが存在しない可能性があることを型情報として明示できます。
存在しないプロパティにアクセスした場合には通常はコンパイルエラーが出ますが、オプショナルプロパティへのアクセスはコンパイルエラーとはならず、undefinedとなります。

type Obj = {str: string, num?: number}

const obj: Obj = {str: "one"}

console.log(obj.str)// "one"
console.log(obj.num)// undefined

上のコード例では、コンパイルエラーが発生することなく、objに存在しないプロパティnumへアクセスできています。
Objの型をVSCodeなどでカーソルを合わせて確認してみると、以下のようにundefinedとのユニオン型が付加されています。

type Obj = {
    str: string;
    num?: number | undefined;
}

このプロパティは一見 num: number | undefined としても同じに思えますが、そのプロパティが存在しないことが許されるかという違いがあります。

ユニオン型

ユニオン型は「または」を表す型の記法で、| を用いて T | U のように記述します。T | U は"T型またはU型"である型を表します。

type T = {str: string, num: number}
type U = {str: string, bool: boolean}

const obj: T | U = {str: "one", num: 1}
const obj2: T | U = {num: 1, bool: true}//T型でもU型でもない場合にはコンパイルエラーとなる。
//Type '{ num: number; bool: true; }' is not assignable to type 'T | U'.
//Property 'str' is missing in type '{ num: number; bool: true; }' but required in type 'U'.(2322)

undefinedとのユニオン型とオプショナルプロパティ

undefinedとのユニオン型T | undefinedは"T型またはundefined型"である型を表します。オブジェクトのプロパティにこの型がついている場合、このプロパティはundefinedであっても問題ありませんが、プロパティが存在しないことによってundefinedが割り当てられる場合にはコンパイルエラーとなります。

type T = { str?: string , num: number}
type U = { str: string | undefined , num:number}

const tObj: T = { num: 1}//tObjについてはコンパイルエラーが出ない。
const uObj: U = { num: 1}
//Property 'str' is missing in type '{ num: number; }' but required in type 'U'.(2741)

const uObj2: U = { num: 1, str: undefined} //undefinedを割り当てればコンパイルエラーとはならない。

上のコード例からもわかるように、オプショナルプロパティについてはオブジェクトに存在していなくてもエラーとなりませんが、undefinedとのユニオン型の場合にはプロパティが存在しない場合にエラーとなります。
undefinedであってもよいが、オブジェクトに必ず存在していてほしいようなプロパティがあるときには、undefinedとのユニオン型の出番ですね。

うぱうぱ

12/15 リテラル型と型推論のwidening

リテラル型

string型やnumber型などの型はプリミティブ型と呼ばれますが、このプリミティブ型よりもさらに細かい型として、プリミティブ型の部分型であるリテラル型があります。'foo'123などがリテラル型にあたり、この型を持つ変数にはリテラル型として指定した値のみが代入可能となります。

type T = 'octopus'

const str: T = 'octopus'

//Type '"squid"' is not assignable to type '"octopus"'.(2322)
const str2: T = 'squid'

上の例は、リテラル型であるoctopus型の変数にoctopus以外の文字列が代入できない例となっています。
プリミティブ型であるboolean型にも、その部分型にtrue型とfalse型というリテラル型が存在します。boolean型はリテラル型のユニオン型true | falseと同じ型となっています。

const a :boolean = true
//const b: boolean と推論される
const b :true | false = true

テンプレートリテラル型

文字列のリテラル型にはテンプレートリテラル型というものもあり、文字列が特定の形をしていることを明示することができます。文字列のテンプレートと同様に、`${}`という書き方でテンプレートリテラル型は記述されます。

type Cephalopod = 'octopus' | 'squid'

type T = `${string} is not fish`

//type U = "octopus is not fish" | "squid is not fish"
type U = `${Cephalopod} is not fish`

const tStr: T = 'tuna is not fish'

//Type '"tuna is not fish"' is not assignable to type '"octopus is not fish" | "squid is not fish"'.(2322)
const uStr: U = 'tuna is not fish'

リテラル型のwidening

リテラル型に型推論が働く場合に、推論結果がプリミティブ型へ拡張される場合があります。この働きはリテラル型のwideningと呼ばれています。プリミティブ型へ拡張されるのは、リテラル型で指定した値以外が再代入されることが想定されるものが対象となっています(たとえばlet変数やオブジェクトリテラルなどが該当します)。

//const constStr: "octopus"
const constStr = 'octopus'

//let letStr: string
let letStr = 'octopus'

wideningが働くのは型推論のときですので、あらかじめ型注釈やas constをつけておけばwideningされずにリテラル型を使用することが可能です。

type T = 'octopus'

//let letStr: "octopus"
let letStr:T = 'octopus'

//let letStr2: "octopus"
let letStr2 = 'octopus' as const
うぱうぱ

12/18 ユーザー定義型ガード

型ガード

型ガードとはif(typeof value === 'string')のように型による条件分岐やin演算子などによってブロック内の型を絞り込む機能を指します。この絞り込み部分をユーザー(実装者)が関数の形で作成して、型ガードとして利用するのがユーザー定義型ガードです。

ユーザー定義型ガード

ユーザー定義型ガードとは、条件を満たした時に関数の引数の型をユーザーが設定した型としてTypeScriptのコンパイラに扱わせる機能です。
asによる型アサーションやany型と同様にTypeScriptの型安全性を破壊しうる危険な機能ですが、確認すべき範囲がasanyに比べて明確であることから、これらの機能をやむを得ず使用する場合にはユーザー定義型ガードの使用が推奨されています。

引数 is 型によるユーザー定義型ガード

関数によって型の絞り込みを行いたい場合には、ユーザー定義型ガードが使用できます。とくに、unknown型の値を扱うのに便利な機能となっています。

function isNumberOrString(value: unknown){
  return typeof value === 'number' || typeof value === 'string' 
}

const value: unknown = "314"
if(isNumberOrString(value)){
    //'value' is of type 'unknown'.(18046)
    console.log(value.toString())
}

上のコード例のように関数による型の条件分岐を行なっても、TypeScriptのコンパイラは型の絞り込みを認識できず、コンパイルエラーとなってしまいます。
そこで、関数の返り値の型にvalue is string | numberとつけることで、関数がtrueを返した場合にはコンパイラにvaluestring | number型であると認識させることができます。ここの実装を間違えると誤った型をコンパイラが認識したままの状態になり、型安全性が損なわれるので注意が必要です。

function isNumberOrString(value: unknown): value is string | number {
  return typeof value === 'number' || typeof value === 'string' 
}

const value: unknown = "314"
if(isNumberOrString(value)){
    //const value: string | number
    console.log(value.toString())// 314
}

asserts 引数 is 型によるユーザー定義型ガード

関数が例外を投げて終了しない可能性がある場合には、asserts 引数 is 型の構文によるユーザー定義型ガードが使えます。
こちらは関数がtrueを返した場合に型を強制するのではなく、関数が最後の処理まで到達した場合に型を強制します。

function isNumber(value: unknown): asserts value is number {
    if(typeof value !== 'number'){
        throw new Error()
    }
    return;
}

const value: unknown = 314
try{
    // const value: unknown
    console.log(value)// 314

    isNumber(value)

    //const value: number
    console.log(value.toString(16))// "13a" 
}catch(error){
    console.log('error occurred')
}

tryブロック内ではisNumber以降の箇所でのvalueの型がnumber型として扱われています。
ユーザーが「isNumberが正常終了しているのならばvalueの型はnumber型である」と保証しているわけですが、ここの保証の部分、つまりユーザー定義型ガードの実装に誤りがあると型安全性は大きく損なわれてしまうため、慎重に使用する必要があります。
anyや型アサーションに比べると、ユーザーが責任を負うべき箇所が明確なので、やむを得ない場合にはユーザー定義型ガードの使用を先に検討することが推奨されているわけですね。

うぱうぱ

12/19 Mapped Typesの基礎

Mapped Typesはユニオン型とプロパティ値の型を指定することで、ユニオン型を構成する各要素をキーとして、指定した値の型をもつオブジェクト型を設定できる機能で、{[P in T]: U}と言う構文で表されます。
インデックスシグネチャ{[key: K]: U}に似た構文を持ちますが、プロパティのキーがTの部分型であるという制約が付いている点が異なります。
インデックスシグネチャにはプロパティ名を動的に決めた場合に存在しないプロパティへのアクセスがコンパイルエラーとならない、という型安全性への問題がありますが、Mapped Typesにはそのような問題がありません。

in演算子

{[P in T]: U}に登場するin T部分で使用できる型はstring | number | symbolの部分型である必要があります。
string | number | symbol、はkeyofが取りうるあらゆる型を部分型にもつ、いわゆるkeyofの上界に当たる型です。

type A = {num: number, str: string}

//Type 'A' is not assignable to type 'string | number | symbol'.(2322)
type B = {[num in A]:number}

// type C = {
//     num: number;
//     str: number;
// }
type C = {[num in keyof A]:number}

{[P in T]: U}の例

type T = {
    num: number
    str: string
    bool: boolean
}

// type Obj = {
//     num: "num"[];
//     str: "str"[];
//     bool: "bool"[];
// }
type Obj = {
    [P in keyof T]: P[]
}

// type CopyOfT = {
//     num: number;
//     str: string;
//     bool: boolean;
// }
type CopyOfT = {
    [P in keyof T]: T[P]
}

Obj型はkeyof T、つまり"num" | "str" | "bool"というユニオン型について、「各構成要素をキー名に持ち、値がそれぞれその要素の配列型P[]となるプロパティ」をもつオブジェクト型となります。

mapped とは?

mapは数学の用語では写像となります。
写像とは、集合の各要素について、対応する要素を決める規則のことです。
Mapped Typesはx|y|zという集合の各要素xyz{x:f(x), y:f(y), z:f(z)}に写像fで写していると捉えると、mappedという名前が相応しいように思えますね。

うぱうぱ

12/20 Conditional Typesの基礎

Conditional Typesは型の条件分岐を行える機能で、T extends U ? A : Bという構文で表されます。三項演算子と同様に、T型がU型の部分型であればA型に、そうでなければB型になります。

type IsString<T> = T extends string ? true : false

//type NumIsString = false
type NumIsString = IsString<number>

//type LiteralIsString = true
type LiteralIsString = IsString<'str'>

ユニオン型と分配法則

Conditional Typesの構文T extends U ? A : BTの部分にユニオン型が指定された場合には、分配が行われる点には注意が必要です。

type ToArray<T> = T extends string ? T[] : never

//type UnionArray = "octopus"[] | "squid"[]
type UnionArray = ToArray<"octopus"|"squid"> 

UnionArrayの型としては、構文を文字通りみていくと"(octopus"|"squid")[]という型になりそうですが、実際にはユニオン型の構成要素が分配される形となり、"octopus"[] | "squid"[]型となります。
"(octopus"|"squid")[]のようなユニオン型の配列型は、以下のコード例のような記述で取得可能です。

type X<T> = T extends string ? [T] : never
//type Y = ("octopus" | "squid")[]
type Y = X<"octopus" | "squid" >[number][]
うぱうぱ

12/23 Homomorphic Mapped Types と準同型写像

Mapped Types{[P in T]: U}のうち、Tの部分がオブジェクトのキーとなっている形{[P in keyof T]: U}のものはHomomorphic Mappd Types と呼ばれます。
構造を保存する写像である、準同型写像(homomorphic mapping)が名前の由来となっています。
Homomorphic Mapped Types{[P in keyof T]: U}は、作成元となるオブジェクト型Tと全く同じプロパティキーをもつため、オブジェクトの構造が保存されていることを表しているようですね。

A mapped type of the form { [P in keyof T]: X } is homomorphic with T (because it has the same set of properties as T) and now preserves the optional and readonly modifiers as they exist on the properties in T.

https://github.com/microsoft/TypeScript/pull/12563

Homomorphic Mapped Typesとプロパティ修飾子の保存

オブジェクト型のプロパティにはreadonly?といったプロパティ修飾子(property modifier)を指定できます。
Homomorphic Mapped Typesにはこの修飾子を保存する機能が備わっています。

type Obj = {
    readonly num: number
    str ?: string
}

type Homomorphic = {
    [PropertyKey in keyof Obj]: Obj[PropertyKey]
}
// type Homomorphic = {
//     readonly num: number;
//     str?: string | undefined;
// }

type keyStr = "num" | "str" 

type NonHomomorphic = {
    [PropertyKey in keyStr]: Obj[PropertyKey]
}
// type NonHomomorphic = {
//     num: number;
//     str: string | undefined;
// }

上のコードでは、Homomorphic Mapped Types を使用した場合には確かに修飾子が保存されていますが、keyof Objから得られる型と同じ型"num" | "str"によって得たMapped Types型を確認してみるとプロパティ修飾子が外れた型となっていることがわかります。

このスクラップは2023/12/25にクローズされました