TypeScript。高度なタイプ

画像



こんにちは住民!別のノベルティ

ProfessionalTypeScript。スケーラブルなJavaScriptアプリケーションの開発を印刷会社に提出しましたこの本では、すでにJavaScriptの中間にいるプログラマーが、TypeScriptを習得する方法を学びます。TypeScriptを使用すると、コードを10倍までスケールアップし、プログラミングを再び楽しくすることができます。



以下は、「AdvancedTypes」という本の章からの抜粋です。



高度なタイプ



世界的に有名なTypeScriptタイプのシステムは、その機能でHaskellプログラマーでさえ驚かされます。ご存知のように、表現力だけでなく使いやすさもあります。タイプの制限と関係は簡潔でわかりやすく、ほとんどの場合自動的に推測されます。



これにバインドされたプロトタイプ、関数のオーバーロード、絶えず変化するオブジェクトなどの動的JavaScript要素をモデル化するには、タイプシステムと、Batmanと同じくらい豊富な演算子が必要です。



この章は、サブタイピング、互換性、分散、ランダム変数、および拡張のトピックについて深く掘り下げることから始めます。次に、詳細化や全体性など、フローベースのタイプチェックの詳細について詳しく説明します。次に、タイプレベルでいくつかの高度なプログラミング機能を示します。オブジェクトタイプの接続とマッピング、条件付きタイプの使用、タイプ保護の定義、およびタイプアサーションや明示的な割り当てアサーションなどのフォールバックソリューションです。最後に、型の安全性を強化するためのいくつかの高度なパターンを紹介します。コンパニオンオブジェクトのパターン化、タプルのインターフェイスの強化、公称型の模倣、安全なプロトタイプの拡張です。



タイプ間の



関係TypeScriptの関係を詳しく見てみましょう。



サブタイプとスーパータイプ



互換性については、「タイプについて」セクションですでに触れました。34では、サブタイプの定義から始めて、このトピックに飛び込みましょう。

画像




図に戻る。3.1およびTypeScriptの組み込みサブタイプの関連付けを参照してください。

画像




  • 配列はオブジェクトのサブタイプです。
  • タプルは配列のサブタイプです。
  • すべてがいずれかのサブタイプです。
  • すべてのサブタイプになることはありません。
  • Animalクラスを拡張するBirdクラスは、Animalクラスのサブタイプです。


サブタイプに与えた定義によると、これは次のことを意味します。



  • オブジェクトが必要な場合はいつでも、配列を使用できます。
  • 配列が必要な場合はいつでも、タプルを使用できます。
  • 必要な場所ならどこでも、オブジェクトを使用できます。
  • どこでも使用することはできません。
  • 動物が必要な場所ならどこでも、鳥を使うことができます。


スーパータイプはサブタイプの反対です。



SUPERTYPE



AとBの2つのタイプがあり、BがAのスーパータイプである場合、Bが必要な場所であればどこでも安全にAを使用できます(図6.2)。


画像


そして再び、図の図に基づいて。3.1:



  • アレイはタプルのスーパータイプです。
  • オブジェクトは配列のスーパータイプです。
  • どれもすべてのスーパータイプです。
  • 決して誰のスーパータイプでもありません。
  • 動物は鳥のスーパータイプです。


これはサブタイプの正反対であり、それ以上のものではありません。



バリエーション



ほとんどのタイプでは、特定のタイプAがBのサブタイプであるかどうかを理解するのは簡単です。数字、文字列などの単純なタイプについては、図の図を参照できます。3.1または独立してユニオン番号に含まれる番号を決定する| stringは、このユニオンのサブタイプです。



ただし、ジェネリックなど、より複雑なタイプがあります。次の質問を検討してください。



  • Array <A>がArray <B>のサブタイプになるのはいつですか?
  • フォームAがフォームBのサブタイプになるのはいつですか。
  • 関数(a:A)=> Bが関数(c:C)=> Dのサブタイプであるのはいつですか?


他のタイプを含むタイプ(つまり、Array <A>のようなタイプパラメーター、{a:number}のようなフィールドを持つフォーム、または(a:A)=> Bのような関数を持つ)のサブタイピングルールは、そうではないため、理解するのが困難です。異なるプログラミング言語間で一貫しています。



次のルールを読みやすくするために、TypeScriptでは機能しないいくつかの構文要素を示します(心配しないでください。数学的なものではありません)。



  • A <:Bは、「AはタイプBと同じサブタイプ」を意味します。
  • A>:Bは、「AはタイプBと同じスーパータイプ」を意味します。


フォームと配列のバリエーション



複雑なタイプをサブタイプ化するためのルールで言語が一致しない理由を理解するには、アプリケーションでユーザーを説明するフォームの例が役立ちます。私たちはそれをいくつかのタイプで表現します:



//  ,   .
type ExistingUser = {
    id: number
   name: string
}
//  ,     .
type NewUser = {
   name: string
}


あなたの会社のインターンがユーザーを削除するためのコードを書くことを任されているとしましょう。彼は次のことから始めます。



function deleteUser(user: {id?: number, name: string}) {
    delete user.id
}
let existingUser: ExistingUser = {
    id: 123456,
    name: 'Ima User'
}
deleteUser(existingUser)


deleteUserは、タイプ{id?:number、name:string}のオブジェクトを受け取り、タイプ{id:number、name:string}のexistingUserを渡します。プロパティIDのタイプ(number)は、予期されるタイプ(number | undefined)のサブタイプであることに注意してください。したがって、{id:number、name:string}オブジェクト全体が{id?:number、name:string}のサブタイプであるため、TypeScriptではこれが許可されます。



セキュリティ上の問題はありますか?1つあります。ExistingUserをdeleteUserに渡した後、TypeScriptはユーザーIDが削除されたことを認識しません。したがって、deleteUser(existingUser)を削除した後にexistingUser.idを読み取ると、TypeScriptはexistingUser.idがタイプ番号であると見なします。



明らかに、スーパータイプが期待されるオブジェクトタイプを使用することは安全ではありません。では、なぜTypeScriptがこれを許可するのでしょうか。肝心なのは、それが完全に安全であることを意図していなかったということです。彼のタイプシステムは、実際のエラーをキャッチし、すべてのレベルのプログラマーに見えるようにすることを目指しています。破壊的な更新(プロパティの削除など)は実際には比較的まれであるため、TypeScriptは緩和され、スーパータイプが期待される場所にオブジェクトを割り当てることができます。



そして、反対の場合はどうですか?そのサブタイプが期待される場所にオブジェクトを割り当てることは可能ですか?



古いユーザーに新しいタイプを追加してから、そのタイプのユーザーを削除しましょう(同僚が書いたコードにタイプを追加することを想像してください)。



type LegacyUser = {
    id?: number | string
    name: string
}
let legacyUser: LegacyUser = {
    id: '793331',
    name: 'Xin Yang'
}
deleteUser(legacyUser) //  TS2345: a  'LegacyUser'
                                  //    
                                  // '{id?: number |undefined, name: string}'.
                                 //  'string'    'number |
                                 // undefined'.


タイプが期待されるタイプのスーパータイプであるプロパティを持つフォームを送信すると、TypeScriptは誓います。これは、idが文字列であるためです|番号| undefinedおよびdeleteUserは、idがnumberの場合のみを処理します|未定義。



フォームを期待している間、期待されるタイプの<:であるプロパティタイプを持つタイプを渡すことはできますが、期待されるタイプのスーパータイプであるプロパティタイプなしでフォームを渡すことはできません。型について話すとき、「TypeScriptフォーム(オブジェクトとクラス)は、それらのプロパティの型において共変です」と言います。つまり、オブジェクトAをオブジェクトBに割り当てるには、その各プロパティが<:Bの対応するプロパティである必要があります。



共分散は、次の4種類の分散のいずれか です。



不変性

特にTが必要です。

共分散

必要<:T。

Contravariance

必要>:T.

バイバリアンスは

、<:Tまたは>:Tのいずれかに適合します。



TypeScriptでは、すべての複合型は、そのメンバー(オブジェクト、クラス、配列、および関数戻り型)で共変ですが、1つの例外があります。関数パラメーターの型は、逆変です。



. , . ( ). , Scala, Kotlin Flow, , .



TypeScript : , , , (, id deleteUser, , , ).


関数のバリエーション



いくつかの例を考えてみましょう。



関数Aは、Aのアリティ(パラメータの数)がBと同じかそれより少ない場合、関数Bのサブタイプです。



  1. Aに属するthis型は未定義であるか、>:Bに属するthis型です。
  2. 各パラメーターA>:Bの対応するパラメーター。
  3. リターンタイプA <:リターンタイプB。


関数Aが関数Bのサブタイプであるためには、そのタイプとパラメーターは>:Bの対応物である必要があり、その戻りタイプは<:である必要があることに注意してください。なぜ状態が逆転するのですか?オブジェクト、配列、ユニオンなどの場合のように、単純な<:条件が各コンポーネント(タイプthis、パラメータータイプ、およびリターンタイプ)で機能しないのはなぜですか?



3つのタイプを定義することから始めましょう(クラスの代わりに、他のタイプを使用できます。ここで、A:<B <:C):



class Animal {}
class Bird extends Animal {
    chirp() {}
}
class Crow extends Bird {
    caw() {}
}


だからカラス<:鳥<:動物。



Birdを取得してツイートする関数を定義しましょう。



function chirp(bird: Bird): Bird {
    bird.chirp()
    return bird
}


ここまでは順調ですね。TypeScriptでは何をチャープにパイプできますか?



chirp(new Animal) //  TS2345:   'Animal'
chirp(new Bird) //     'Bird'.
chirp(new Crow)


Birdインスタンス(birdタイプのchirpパラメーターとして)またはCrowインスタンス(Birdのサブタイプとして)。サブタイプの受け渡しは期待どおりに機能します。



新しい関数を作成しましょう。今回は、そのパラメーターは関数になります。



function clone(f: (b: Bird) => Bird): void {
    // ...
}


cloneには、Birdを取得してBirdを返す関数fが必要です。どのような種類の関数をfに安全に渡すことができますか?明らかに、Birdを受信して​​返す関数:



function birdToBird(b: Bird): Bird {
    // ...
}
clone(birdToBird) // OK


鳥を連れてカラスや動物を返す関数はどうですか?



function birdToCrow(d: Bird): Crow {
    // ...
}
clone(birdToCrow) // OK
function birdToAnimal(d: Bird): Animal {
    // ...
}
clone(birdToAnimal) //  TS2345:   '(d: Bird) =>
                             // Animal'    
                            // '(b: Bird) => Bird'. 'Animal'
                           //    'Bird'.


birdToCrowは期待どおりに機能しますが、birdToAnimalはエラーをスローします。どうして?クローンの実装が次のようになっていると想像してください。



function clone(f: (b: Bird) => Bird): void {
    let parent = new Bird
    let babyBird = f(parent)
    babyBird.chirp()
}


関数fをcloneに渡すと、Animalが返され、その中で.chirpを呼び出すことはできません。したがって、TypeScriptは、渡す関数が少なくともBirdを返すことを確認する必要があります。



関数がリターンタイプで共変であると言うとき、それは、そのリターンタイプが<:その関数のリターンタイプである場合にのみ、関数が別の関数のサブタイプになることができることを意味します。



では、パラメータタイプはどうですか?



function animalToBird(a: Animal): Bird {
  // ...
}
clone(animalToBird) // OK
function crowToBird(c: Crow): Bird {
  // ...
}
clone(crowToBird)        //  TS2345:   '(c: Crow) =>
                        // Bird'     '
                       // (b: Bird) => Bird'.


関数が別の関数と互換性を持つためには、そのすべてのパラメータータイプ(これを含む)が>:他の関数の対応するパラメーターである必要があります。理由を理解するために、クローンに渡す前に、ユーザーがcrowToBirdを実装する方法を考えてみてください。



function crowToBird(c: Crow): Bird {
  c.caw()
  return new Bird
}


TSC-: STRICTFUNCTIONTYPES



- TypeScript this. , , {«strictFunctionTypes»: true} tsconfig.json.



{«strict»: true}, .


これで、クローンが新しいBirdでcrowToBirdを呼び出すと、例外が発生します。これは、.cawがすべてのCrowで定義されているが、すべてのBirdでは定義されていないためです。



これは、関数のパラメーターとこのタイプが逆変であることを意味します。つまり、関数は、そのパラメーターのそれぞれとタイプが>:他の関数の対応するパラメーターである場合にのみ、別の関数のサブタイプになることができます。



幸い、これらのルールを覚えておく必要はありません。間違って入力された関数をどこかに渡したときにエディターが赤い下線を付けるときは、それらを覚えておいてください。



互換性



サブタイプとスーパータイプの関係は、静的に型付けされた言語の重要な概念です。これらは、互換性がどのように機能するかを理解するためにも重要です(互換性とは、タイプBが必要な場合にタイプAの使用を管理するTypeScriptルールを指します)。



TypeScriptが「タイプAはタイプBと互換性がありますか?」という質問に答える必要がある場合、それは単純なルールに従います。配列、ブール値、数値、オブジェクト、関数、クラス、クラスインスタンス、文字列を含む文字列などの非列挙型の場合、条件の1つが真であれば、AはBと互換性があります。



  1. A <:B。
  2. Aはどれでも。


ルール1は単なるサブタイプの定義です。AがBのサブタイプである場合、Bが必要な場合はいつでも、Aを使用できます。



ルール2は、JavaScriptコードとの対話を容易にするためのルール1の例外です。

キーワードenumまたはconstenumによって作成された列挙型の場合、条件の1つが真であれば、タイプAは列挙Bと互換性があります。



  1. Aは列挙型Bのメンバーです。
  2. Bにはタイプ番号のメンバーが少なくとも1つあり、Aは番号です。


ルール1は、単純型の場合とまったく同じです(AがBのメンバーである場合、Aは型Bであり、B <:Bと言います)。



ルール2は、TypeScriptのセキュリティを著しく損なう列挙を操作するために必要です(60ページのサブセクション「列挙」を参照)。これらは避けることをお勧めします。



タイプ拡張タイプ



拡張は、タイプ推論がどのように機能するかを理解するための鍵です。 TypeScriptは実行に寛容であり、可能な限り具体的なものを推測するよりも、より一般的な型を推測する際に誤りを犯す可能性が高くなります。これにより、作業が楽になり、タイプチェッカーノートの処理にかかる時間が短縮されます。



第3章では、型拡張のいくつかの例をすでに見てきました。他の人を考慮してください。



変数を可変として宣言すると(letまたはvarを使用)、その型はそのリテラルの値型から、そのリテラルが属する基本型に拡張されます。



let a = 'x' // string
let b = 3   // number
var c = true   // boolean
const d = {x: 3}   // {x: number}
enum E {X, Y, Z}
let e = E.X   // E


これは不変の宣言には適用されません。



const a = 'x' // 'x'
const b = 3   // 3
const c = true   // true
enum E {X, Y, Z}
const e = E.X   // E.X


明示的な型注釈を使用して、展開されないようにすることができます。



let a: 'x' = 'x' // 'x'
let b: 3 = 3  // 3
var c: true = true  // true
const d: {x: 3} = {x: 3}  // {x: 3}


拡張されていない型をletまたはvarで再割り当てすると、TypeScriptがそれを拡張します。これを防ぐには、元の宣言に明示的な型注釈を追加します。



const a = 'x' // 'x'
let b = a  // string
const c: 'x' = 'x'  // 'x'
let d = c  // 'x'


nullまたは未定義に初期化された変数は、次のいずれかに展開されます。



let a = null // any
a = 3  // any
a = 'b'  // any


ただし、nullまたは未定義に初期化された変数が宣言されたスコープを離れると、TypeScriptはその変数に特定のタイプを割り当てます。



function x() {
   let a = null  // any
   a = 3   // any
   a = 'b'   // any
   return a
}
x()   // string


const



タイプconstタイプは、タイプ宣言の拡張を回避するのに役立ちます。タイプアサーションとして使用します(185ページのサブセクション「タイプの承認」を参照)。



let a = {x: 3}   // {x: number}
let b: {x: 3}    // {x: 3}
let c = {x: 3} as const   // {readonly x: 3}


constは、深くネストされたデータ構造であっても、型の展開を排除し、そのメンバーを読み取り専用として再帰的にマークします。



let d = [1, {x: 2}]              // (number | {x: number})[]
let e = [1, {x: 2}] as const    // readonly [1, {readonly x: 2}]


TypeScriptで可能な限り狭く推定する場合は、constとして使用します。



追加のプロパティのチェック



TypeScriptが、あるタイプのオブジェクトが別のタイプのオブジェクトと互換性があるかどうかをチェックするときにも、タイプ拡張が機能します。



オブジェクトタイプは、メンバー内で共変です(148ページの「形状と配列のバリエーション」サブセクションを参照)。ただし、TypeScriptが追加のチェックなしでこのルールに従うと、問題が発生する可能性があります。



たとえば、クラスに渡してカスタマイズできるOptionsオブジェクトについて考えてみます。



type Options = {
    baseURL: string
    cacheSize?: number
    tier?: 'prod' | 'dev'
}
class API {
    constructor(private options: Options) {}
}
new API({
     baseURL: 'https://api.mysite.com',
     tier: 'prod'
})


オプションを間違えた場合はどうなりますか?



new API({
   baseURL: 'https://api.mysite.com',
   tierr: 'prod'         //  TS2345:   '{tierr: string}'
})                      //     'Options'.
                        //     
                       //  ,  'tierr'  
                      //   'Options'.    'tier'?


これは一般的なJavaScriptのバグであり、TypeScriptがそれをキャッチするのに役立つのは良いことです。しかし、オブジェクトのタイプがメンバー内で共変である場合、TypeScriptはどのようにそれをインターセプトしますか?



言い換えると:



  • タイプ{baseURL:string、cacheSize?:number、tier?: 'prod' | 'dev'}。
  • タイプ{baseURL:string、tierr:string}を渡しました。
  • 渡されたタイプは予期されたタイプのサブタイプですが、TypeScriptはエラーを報告することを知っていました。


追加のプロパティをチェックする ことにより、新しいオブジェクトリテラルタイプTを別のタイプUに割り当てようとすると、TにはUにないプロパティがあり、TypeScriptはエラーを報告します。



新しいオブジェクトリテラルタイプは、TypeScriptがオブジェクトリテラルから推測したタイプです。このオブジェクトリテラルがタイプアサーションを使用する場合(サブセクション「タイプアサーション」(185ページ)を参照)、または変数に割り当てられる場合、新しいタイプは通常のオブジェクトタイプに展開され、その新規性は失われます。



この定義をより容量の大きいものにしてみましょう。



type Options = {
     baseURL: string
     cacheSize?: number
     tier?: 'prod' | 'dev'
}
class API {
    constructor(private options: Options) {}
}
new API({ ❶
    baseURL: 'https://api.mysite.com',
    tier: 'prod'
})
new API({ ❷
    baseURL: 'https://api.mysite.com',
    badTier: 'prod' //  TS2345:   '{baseURL:
}) // string; badTier: string}' 
//    'Options'.
new API({ ❸
    baseURL: 'https://api.mysite.com',
    badTier: 'prod'
} as Options)
let badOptions = { ❹
    baseURL: 'https://api.mysite.com',
    badTier: 'prod'
}
new API(badOptions)
let options: Options = { ❺
    baseURL: 'https://api.mysite.com',
    badTier: 'prod' //  TS2322:  '{baseURL: string;
} // badTier: string}'  
// 'Options'.
new API(options)


❶baseURLと2つのオプションプロパティの1つであるtierを使用してAPIをインスタンス化します。すべてが機能しています。



❷ティアを誤ってbadTierと記述しました。新しいAPIに渡すoptionsオブジェクトは新しい(タイプが推測され、変数と互換性がなく、アサーションを入力しません)。したがって、不要なプロパティをチェックするときに、TypeScriptは余分なbadTierプロパティ(optionsオブジェクトで定義されていますが、タイプオプションではありません)。



❸無効なオプションオブジェクトのタイプがOptionsであることを記述します。 TypeScriptはそれを新しいものと見なさなくなり、追加のプロパティのチェックからエラーがないと結論付けます。 as T構文については、p。の「タイプアサーション」セクションで説明しています。 185。



❹optionsオブジェクトをbadOptions変数に割り当てます。 TypeScriptはそれを新しいものとして認識しなくなり、不要なプロパティをチェックした後、エラーはないと結論付けます。



❺optionsをOptionsとして明示的に入力すると、optionsに割り当てるオブジェクトは新しいため、TypeScriptは追加のプロパティをチェックし、バグを見つけます。この場合、オプションを新しいAPIに渡すときに追加のプロパティのチェックは実行されませんが、optionsオブジェクトをoptions変数に割り当てようとするときにチェックが実行されることに注意してください。



これらのルールを覚える必要はありません。これは、できるだけ多くのバグをキャッチするための内部TypeScriptヒューリスティックです。 TypeScriptが、あなたの会社の昔の人であり、パートタイムのプロのコード検閲者であるIvanでさえ気づかなかったバグをどのようにして見つけたのか、突然疑問に思った場合は、それらを覚えておいてください。 TypeScriptの



改良により



、シンボリック型の推論が実行されます。型チェックモジュールは、コマンドフロー命令(if、?、||、switchなど)と型クエリ(typeof、instanceof、inなど)を使用して、プログラマーがコードを読み取るときに型を修飾します。ただし、この便利な機能はごく少数の言語でサポートされています。



TypeScriptでCSSルールを定義するためのAPIを開発し、同僚がそれを使用してHTML要素の幅を設定したいとします。後で解析して確認する幅を伝えます。



まず、CSS文字列を値と単位に解析する関数を実装しましょう。



//       
//  ,      CSS
type Unit = 'cm' | 'px' | '%'
//   
let units: Unit[] = ['cm', 'px', '%']
//   . .   null,    
function parseUnit(value: string): Unit | null {
  for (let i = 0; i < units.length; i++) {
    if (value.endsWith(units[i])) {
       return units[i]
}
}
     return null
}


次に、parseUnitを使用して、ユーザーが指定した幅を解析します。幅は、数値(おそらくピクセル単位)、または単位が付加された文字列、null、または未定義にすることができます。



この例では、型修飾を数回使用します。



type Width = {
     unit: Unit,
     value: number
}
function parseWidth(width: number | string | null |
undefined): Width | null {
//  width — null  undefined,  .
if (width == null) { ❶
     return null
}
//  width — number,  .
if (typeof width === 'number') { ❷
    return {unit: 'px', value: width}
}
//      width.
let unit = parseUnit(width)
if (unit) { ❸
return {unit, value: parseFloat(width)}
}
//     null.
return null
}


❶TypeScriptは、JavaScriptのnullに対する緩い同等性が、nullとundefinedの両方に対してtrueを返すことを理解できます。彼はまた、チェックに合格した場合は返品を行い、返品を行わなかった場合はチェックに失敗し、その時点から幅のタイプは数値|であることも知っています。文字列(nullまたは未定義にすることはできなくなりました)。タイプは数から洗練されたと言います|文字列| null |数が未定義|ストリング。



❷typeofチェックは、そのタイプを確認するために実行時に値を要求します。 TypeScriptは、コンパイル時にtypeofも利用します。テストに合格したifブランチでは、TypeScriptは幅が数値であることを認識しています。それ以外の場合(このブランチが返される場合)、幅は文字列である必要があります-残りのタイプは



❸parseUnitはnullを返す可能性があるため、これを確認します。 TypeScriptは、ユニットが正しい場合、ifブランチのタイプがUnitである必要があることを認識しています。それ以外の場合、unitは正しくありません。つまり、そのタイプはnullです(Unit | nullから洗練されています)。



❹最後に、nullを返します。これは、ユーザーが幅の文字列を渡したが、その文字列にサポートされていない単位が含まれている場合にのみ発生する可能性があります。

行われたタイプの改良ごとに、TypeScriptの一連の考え方を調べました。 TypeScriptは、コードを読み書きするときに推論を行い、それを型チェックと推論の順序に結晶化するという素晴らしい仕事をします。



識別された結合タイプ



ちょうどわかったように、TypeScriptはJavaScriptがどのように機能するかをよく理解しており、私たちの心を読んでいるかのように型の資格を追跡することができます。



アプリケーションのカスタムイベントシステムを作成しているとしましょう。まず、イベントの種類と、これらのイベントの到着を処理する関数を定義します。UserTextEventがキーボードイベント(たとえば、ユーザーが<input />と入力した)をシミュレートし、UserMouseEventがマウスイベント(ユーザーがマウスを座標[100、200]に移動した)をシミュレートするとします。



type UserTextEvent = {value: string}
type UserMouseEvent = {value: [number, number]}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
     if (typeof event.value === 'string') {
         event.value // string
         // ...
         return
   }
         event.value // [number, number]
}


TypeScriptは、ifブロック内でevent.valueが文字列である必要があることを認識しています(typeofチェックのおかげで)。つまり、ifブロックの後のevent.valueは[number、number]タプルである必要があります(ifブロックで返されるため)。



合併症はどこにつながるのでしょうか?イベントタイプに説明を追加しましょう。



type UserTextEvent = {value: string, target: HTMLInputElement}
type UserMouseEvent = {value: [number, number], target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
    if (typeof event.value === 'string') {
        event.value // string
        event.target // HTMLInputElement | HTMLElement (!!!)
        // ...
        return
   }
  event.value // [number, number]
  event.target // HTMLInputElement | HTMLElement (!!!)
}


改良はevent.valueに対しては機能しましたが、event.targetに対しては機能しませんでした。どうして?ハンドルがUserEventタイプのパラメーターを受け取った場合、UserTextEventまたはUserMouseEventのいずれかを渡す必要があるという意味ではありません。実際、UserMouseEvent |タイプの引数を渡すことができます。UserTextEvent。また、ユニオンのメンバーは重複する可能性があるため、TypeScriptには、ユニオンのいつ、どのケースが関連するかを知るためのより信頼性の高い方法が必要です。



これを行うには、ユニオンタイプの各ケースにリテラルタイプとタグ定義を使用します。素敵なタグ:



  • いずれの場合も、ユニオンタイプの同じ場所にあります。オブジェクトタイプの結合に関しては同じオブジェクトフィールドを意味し、タプルの結合に関しては同じインデックスを意味します。実際には、識別された組合はより多くの場合対象です。
  • リテラルタイプ(文字列リテラル、数値、ブール値など)として入力されます。さまざまなタイプのリテラルを組み合わせて組み合わせることができますが、単一のタイプに固執することをお勧めします。通常、これは文字列リテラルの一種です。
  • 普遍的ではありません。タグは、汎用タイプの引数を受け取ることはできません。
  • 相互に排他的(ユニオンタイプ内で一意)。


上記を考慮して、イベントのタイプを更新しましょう。



type UserTextEvent = {type: 'TextEvent', value: string,
                                        target: HTMLInputElement}
type UserMouseEvent = {type: 'MouseEvent', value: [number, number],
                                        target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
   if (event.type === 'TextEvent') {
       event.value // string
       event.target // HTMLInputElement
       // ...
       return
   }
  event.value // [number, number]
  event.target // HTMLElement
}


これで、タグ付きフィールド(event.type)の値に基づいてイベントを絞り込むと、TypeScriptはifブランチにUserTextEventが必要であり、ifブランチの後にUserMouseEventが必要であることを認識します。タグは各ユニオンタイプで一意であるため、TypeScriptは認識します。それらは相互に排他的であること。



結合タイプのさまざまなケースを処理する関数を作成するときは、識別された結合を使用します。たとえば、Fluxアクション、reduxリストア、またはReactのuseReducerを操作する場合です。



あなたは本をより詳細に理解し、出版社のウェブサイトで特別価格で事前注文することができます



All Articles