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