Programming TypeScript ch6 (3/3) Advanced Function Types
- Advanced Function Types
- Conditional Types
- Escape Hatches
- Simulating Nominal Types
- Safely Extending the Prototype
- Exercise
Advanced Function Types
Improving Type Inference for Tuples
- TSのtupleの推論はゆるゆる
let a = [1, true] // (number|boolean)[]
- こうすると厳しく推論される
function tuple< T extends unknown[] >( ...ts: T ): T { return ts } let a = [1, true] //(number|boolean)[] let b = tuple(1, true) // [number, boolean]
User-Defined Type Guards
function isString(a: unknown): boolean { return typeof a === 'string' } const a = isString('a') // boolean const b = isString([7]) // boolean function parseInput(input: string | number) { let formattedInput: string if(isString(input)) { // here input is (string|number) formattedInput = input.toUpperCase() // Error: Property 'toUpperCase' does not exist on type 'string | number'. } }
- 静的な情報がないため
input
をrefineできない isString
のreturn typeをa is string
にすることで改善可能
function isString(a: unknown): a is string { return typeof a === 'string' } const a = isString('a') // boolean const b = isString([7]) // boolean function parseInput(input: string | number) { let formattedInput: string if(isString(input)) { // here input is string formattedInput = input.toUpperCase() return } // here input is number input }
Conditional Types
type IsString<T> = T extends string ? true : false type A = IsString<string> // true type B = IsString<number> // false
Distributive Conditionals
- 分配法則
- これは、それはそう
type ToArray<T> = T[] type A = ToArray<number> // number[] type B = ToArray<number|string> // (number|string)[]
- conditional typesを使うと
type ToArray2<T> = T extends unknown ? T[] : T[] type C = ToArray2<number> // number[] type D = ToArray2<number|string> // number[]|string[]
- union typesがconditionalのbranchにバラされる
- union typesから所定の型を取り除く
Without<T,U>
なんかを作れる:
type Without<T,U> = T extends U ? never : T type A = Without< boolean | number | string, boolean > // string|number
- 導出
type Without<T,U> = T extends U ? never : T type A = Without< boolean | number | string, boolean > // string|number type A2 = Without<boolean, boolean> | Without<number, boolean> | Without<string, boolean> type A3 = (boolean extends boolean ? never : boolean) | (number extends boolean ? never : number) | (string extends boolean ? never : string)
The infer Keyword
type ElementType<T> = T extends unknown[] ? T[number] : T type A = ElementType<number[]> // number type B = ElementType<boolean> // boolean type ElementType2<T> = T extends (infer U) ? U : T type C = ElementType<number[]> // number type D = ElementType<boolean> // boolean
- コンテキストから
U
が推論される - 関数の引数の情報を静的に取得したりできる
type SecondArg<F> = F extends (a: any, b:infer B) => any ? B : never type F = typeof Array['prototype']['slice'] // (start?: number|undefined, end?: number|undefined) => any[] type A = SecondArg<F> // (number|undefined)
Build-in Conditional Types
Exclude<T, U>
- さっき作ったWithoutとおなじ
type A = number | string | boolean type B = boolean | typeof Array type C = Exclude<A, B> // string | number
Extract<T, U>
- TのうちUに代入可能なものを抽出
type A = number | string | boolean type B = boolean | typeof Array type C = Extract<A, B> // boolean
NonNullable<T>
type A = {a?: number | null} type B = NonNullable<A['a']> // number
- 【補】既存の型のnullableを外した型を得る
type A = { a: string|boolean|null b: number[] } type B = { [K in keyof A]: NonNullable<A[K]> } // { // a: string|boolean|null // b: number[] // }
ReturnType<F>
- 関数
F
の戻り値 - 【補】自分で作るときは引数はany、ないしはボトム型にしないといけない
- 引数は反変なので
- 関数
type F = (a:number) => string type R = ReturnType<F> // string type MyReturnType<F> = F extends (a: any) => (infer R) ? R : void type R2 = MyReturnType<F> // string type MyReturnType2<F> = F extends (a: never) => (infer R) ? R : void type R3 = MyReturnType2<F> // string
Escape Hatches
- 多用せぬこと
- 何かがおかしい証拠
Type Assertions
function formatInput(input: string) { // ... } function getUserInput(): string|number{ // ... } const input = getUserInput() formatInput(input) // Error: Argument of type 'string | number' is not assignable to parameter of type 'string'. formatInput(input as string) formatInput(<string>input)
- tsxのことを考えると
<>
よりas
のほうがよい - 危ないことができるので可能なら避ける
function addToList<T>(list: T[], item:T){ // ... } const list = [1,2,3] addToList(list, 4) addToList('hoge', 'hoge') // Error: Argument of type '"hoge"' is not assignable to parameter of type 'string[]'. addToList('hoge' as any, 'hoge') // passes
Nonnull Assertions
type Dialog = { id?: string } function closeDialog(dialog: Dialog) { if (!dialog.id) { return } // here dialog.id is refined to string dialog.id setTimeout(() => { removeFromDOM( dialog, document.getElementById(dialog.id) // Error: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. ) }) } function removeFromDOM(dialog: Dialog, element:Element) { element.parentNode.removeChild(element) // Error: Object is possibly 'null'. delete dialog.id }
- 潜在的な
null
やundefined
にまつわるエラーdialog.id
: setTimeoutのコールバックなので、他の誰かが書き換えている可能性があり、refinementが効かないdocument.getElementById(id)
: 見つからなければnullelement.parentNode
: undefinedの可能性がある- root
- DOMに追加されていない
null
やundefined
にまつわるエラーを黙らせる演算子!
type Dialog = { id?: string } function closeDialog(dialog: Dialog) { if (!dialog.id) { return } // here dialog.id is refined to string dialog.id setTimeout(() => { removeFromDOM( dialog, document.getElementById(dialog.id!)! ) }) } function removeFromDOM(dialog: Dialog, element:Element) { element.parentNode!.removeChild(element) delete dialog.id }
- 多く現れたら、それはリファクタリングすべき兆候
type VisibleDialog = { id: string } type DestroyedDialog = {} type Dialog = VisibleDialog | DestroyedDialog function closeDialog(dialog: Dialog) { if (!('id' in dialog)) { return } // dialog is refined to VisibleDialog dialog.id setTimeout(() => { removeFromDOM( dialog, document.getElementById(dialog.id)! ) }) } function removeFromDOM(dialog: VisibleDialog, element:Element) { element.parentNode!.removeChild(element) delete dialog.id }
- ビックリを減らせた
Definite Assignment Assertions
let userId: string fetchUser() userId.toUpperCase() // Error: Variable 'userId' is used before being assigned. function fetchUser() { userId = globalCache.get('userId') }
- これもビックリで黙らせられる
let userId!: string fetchUser() userId.toUpperCase() function fetchUser() { userId = globalCache.get('userId') }
Simulating Nominal Types
IS TYPESCRIPT'S TYPE SYSTEM STRUCTURAL OR NOMINAL?
- 構造的型付け: 別の名前でも同じ形なら同じ型
- ゆえに起きる問題:
type CompanyID = string type OrderID = string type UserID = string const userId: UserID = '1234' const companyId: CompanyID = userId // OK (!!!)
- 公称型: 同じ形でも別の名前なら別の型
- TSにおいて、このような型は他にenumがある
- unique Symbolとの交差型で公称型をシミュレートすることができる
type CompanyID = string & { readonly brand: unique symbol } type OrderID = string & { readonly brand: unique symbol } type UserID = string & { readonly brand: unique symbol } // Companion Object Pattern function CompanyID(id: string): CompanyID { return id as CompanyID } function OrderID(id: string): OrderID { return id as OrderID } function UserID(id: string): UserID { return id as UserID } const userId: UserID = UserID('1234') const companyId: CompanyID = userId // Error: Type 'UserID' is not assignable to type 'CompanyID'. const userIdAsString: string = userId // OK
Safely Extending the Prototype
Array.prototype
とかを拡張する
function tuple<T extends unknown[]>(...ts: T): T { return ts } interface Array<T> { zip<U>(list: U[]): [T, U][] } Array.prototype.zip = function <T, U>( this: T[], list: U[] ): [T, U][] { return this.map((v, k) => tuple(v, list[k])) } const a = [1, 2, 3] const b = ['a', 'b', 'c'] const c = a.zip(b) // [number,string][]
- interface merging言語仕様のおかげで
Array.prototype
が拡張されるtype
だとむり
- moduleモードのファイルで
declare global
してもよい
zip.ts
export {} function tuple<T extends unknown[]>(...ts: T): T { return ts } Array.prototype.zip = function <T, U>( this: T[], list: U[] ): [T, U][] { return this.map((v, k) => tuple(v, list[k])) } declare global { interface Array<T> { zip<U>(list: U[]): [T, U][] } }
- ただし、このままだとimportしなくても動作してしまう
const a = [1, 2, 3] const b = ['a', 'b', 'c'] const c = a.zip(b) // OK
- importを強制するためにはexcludeする
tsconfig.json
"exclude": [ "src/zip.ts" ]
const a = [1, 2, 3] const b = ['a', 'b', 'c'] const c = a.zip(b) // Error: Property 'zip' does not exist on type 'number[]'.
import './zip' const a = [1, 2, 3] const b = ['a', 'b', 'c'] const c = a.zip(b) // OK
Exercise
union typesの排他的論理和版をつくる
type ExclusiveUnion<T, U> = Exclude<T | U, T & U> type A = ExclusiveUnion<1 | 2 | 3, 2 | 3 | 4> const a1: A = 1 const a2: A = 2 // Error const a3: A = 3 // Error const a4: A = 4