TypeScript متقدم

الغوص الحر - الغوص بدون الغوص. يشعر الغواص بقانون أرخميدس: إنه يزيح كمية معينة من الماء ، مما يدفعه إلى الوراء. لذلك ، تكون العدادات القليلة الأولى هي الأصعب ، ولكن بعد ذلك تبدأ قوة الضغط لعمود الماء فوقك للمساعدة في التحرك أعمق. تذكرنا هذه العملية بالتعلم والغوص في أنظمة Type Type - تصبح أسهل قليلاً أثناء الغوص. ولكن يجب ألا ننسى أن نظهر في الوقت المناسب.


الصورة من One Ocean One Breath .

ميخائيل باشوروف (سايتوناكامورا) - مهندس الواجهة الأمامية الأول في WiseBits ، مروحة TypeScript وهواة مجانية. إن التشابه بين تعلم TypeScript والغوص العميق ليس من قبيل الصدفة. سيخبرك مايكل ما هي النقابات التمييزية ، وكيفية استخدام الاستدلال النوعي ، ولماذا تحتاج إلى التوافق الاسمي والعلامة التجارية. احبس أنفاسك واغطس.

عرض وروابط لجيثب مع نموذج التعليمات البرمجية هنا .

طبيعة العمل


أنواع الجبر تبدو جبرية - دعنا نحاول معرفة ما هي. يطلق عليهم variant في ReasonML ، والنقابات الموسومة في Haskell ، والنقابات التمييزية في F # و TypeScript.

تقدم ويكيبيديا التعريف التالي: "نوع المبلغ هو مجموع أنواع الأعمال".
- شكرا لك كابتن!

التعريف صحيح تمامًا ، ولكنه عديم الفائدة ، لذا دعنا نفهم. دعنا نذهب من القطاع الخاص ونبدأ بنوع العمل.

افترض أن لدينا نوع Task. لها مجالان: idومن أنشأها.

type Task = { id: number, whoCreated: number }

هذا النوع من العمل هو تقاطع أو تقاطع . هذا يعني أنه يمكننا كتابة نفس رمز كل حقل على حدة.

type Task = { id: number } & { whoCreated: number }

في جبر الاقتراحات ، يسمى هذا الضرب المنطقي: هذه هي العملية "AND" أو "AND". نوع المنتج هو المنتج المنطقي لهذين العنصرين.
يمكن التعبير عن كائن بمجموعة من الحقول من خلال منتج منطقي.
لا يقتصر نوع العمل على الحقول. تخيل أننا نحب النموذج الأولي وقرر أننا مفقودون leftPad.

type RichString = string & { leftPad: (toLength: number) => string }

أضفه إلى String.prototype. للتعبير عن الأنواع ، نأخذ السلسلة والوظيفة التي تأخذ القيم اللازمة. الطريقة والسلسلة هي التقاطع.

جمعية


الاتحاد - الاتحاد - مفيد ، على سبيل المثال ، لتمثيل نوع متغير يحدد عرض عنصر في CSS: سلسلة من 10 بكسل أو قيمة مطلقة. مواصفات CSS في الواقع أكثر تعقيدًا بكثير ، ولكن من أجل البساطة ، دعنا نتركها بهذه الطريقة.

type Width = string | number

مثال أكثر تعقيدا. لنفترض أن لدينا مهمة. يمكن أن يكون في ثلاث ولايات: تم إنشاؤها للتو وقبولها في العمل وإكمالها.

type InitialTask = { id: number, whoCreated: number }
type InWorkTask = { id: number, whoCreated: number }
type FinishTask = { id: number, whoCreated: number, finishDate: Date }

type Task = InitialTask | InWorkTask | FinishedTask

في الحالتين الأولى والثانية ، كانت المهمة idومن أنشأها ، وفي الحالة الثالثة ، يظهر تاريخ الانتهاء. هنا يظهر الاتحاد - إما هذا أو ذاك.

ملاحظة . بدلاً من ذلك ، typeيمكنك استخدامها interface. ولكن هناك فروق دقيقة. دائمًا ما تكون نتيجة الاتحاد والتقاطع من النوع. يمكنك التعبير عن التقاطع من خلال extends، لكن الاتحاد من خلال interfaceلا يمكن. إنه typeأقصر فقط.

interfaceيساعد فقط على دمج الإعلانات. يجب دائمًا التعبير عن أنواع المكتبات interface، لأن هذا سيسمح للمطورين الآخرين بتوسيعها مع حقولهم. لذلك ، على سبيل المثال ، jQuery-plugins.

توافق النوع


ولكن هناك مشكلة - في TypeScript ، توافق النوع الهيكلي . هذا يعني أنه إذا كان النوعان الأول والثاني لهما نفس مجموعة الحقول ، فإن هذين الحقلين من نفس النوع. على سبيل المثال ، لدينا 10 أنواع بأسماء مختلفة ، ولكن مع بنية متطابقة (بنفس الحقول). إذا قدمنا ​​أيًا من الأنواع العشرة لدالة تقبل أحدها ، فلن تمانع TypeScript.

في Java أو C # ، على العكس من ذلك ، نظام التوافق الاسمي . سنكتب نفس الفصول العشرة بحقول متطابقة ووظيفة تأخذ واحدة منها. لن تقبل الدالة فئات أخرى إذا لم تكن من سلالة مباشرة من النوع الذي تهدف إليه الوظيفة.

تتم معالجة مشكلة التوافق الهيكلي بإضافة حالة إلى الحقل: enumأوstring. لكن أنواع المجموع هي طريقة للتعبير عن التعليمات البرمجية بشكل أكثر دلالة من كيفية تخزينها في قاعدة البيانات.

على سبيل المثال ، سنتوجه إلى ReasonML. هكذا يبدو هذا النوع هناك.

type Task =
| Initial({ id: int, whoCreated: int })
| InWork({ id: int, whoCreated: int })
| Finished({
        id: int,
        whoCreated: int,
        finshDate: Date
    })

نفس الحقول ، باستثناء ذلك intبدلاً من ذلك number. إذا نظرت عن كثب ، نلاحظ Initial، InWorkو Finished. لا توجد على اليسار في اسم النوع ، ولكن على اليمين في التعريف. هذا ليس مجرد سطر في العنوان ، ولكنه جزء من نوع منفصل ، لذا يمكننا تمييز الأول عن الثاني.

الإختراق الحياة . بالنسبة للأنواع العامة (مثل كيانات نطاقك) ، أنشئ ملفًا global.d.tsوأضف جميع الأنواع إليه. ستكون مرئية تلقائيًا في جميع أنحاء قاعدة التعليمات البرمجية TypeScript دون الحاجة إلى استيراد صريح. يعد هذا مناسبًا للترحيل لأنه لا داعي للقلق بشأن مكان وضع الأنواع.

دعونا نرى كيفية القيام بذلك ، باستخدام مثال كود Redux البدائي.

export const taskReducer = (state, action) = > {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, erro: action.payload }
    }
    
    return state
}

أعد كتابة الكود في TypeScript. لنبدأ بإعادة تسمية الملف - من .jsإلى .ts. قم بتشغيل كافة خيارات الحالة وخيار noImplicitAny. يعثر هذا الخيار على الوظائف التي لم يتم تحديد نوع المعلمة فيها ، وهو مفيد في مرحلة الترحيل.

قابل للطباعة State: إضافة حقل isFetching، Task(والذي لا يمكن أن يكون) و Error.

type Task = { title: string }

declare module "*.jpg" {
    const url: string
    export default url
}

declare module "*.png" {
    const url: string
    export default url
}

ملحوظة. استراق Lifehack في الغوص العميق بتبسرت علي سيد TypeScript . إنها قديمة بعض الشيء ، لكنها لا تزال مفيدة لأولئك الذين يرغبون في الغوص في TypeScript. اقرأ عن الحالة الحالية لـ TypeScript على مدونة Marius Schulz. يكتب عن الميزات والحيل الجديدة. أيضًا دراسة سجلات التغيير ، لا توجد معلومات حول التحديثات فحسب ، بل أيضًا كيفية استخدامها.

الإجراءات المتبقية. سنكتب ونعلن كل منهم.

type FetchAction = {
    type: "TASK_FETCH"
}

type SuccessAction = {
    type: "TASK_SUCCESS",
    payload: Task

type FailAction = {
    type: "TASK_FAIL",
    payload: Error
}

type Actions = FetchAction | SuccessAction | FailAction

يختلف المجال typeلجميع الأنواع ، فهو ليس مجرد سلسلة ، بل سلسلة محددة - سلسلة حرفية . هذا هو التمييز - العلامة التي نميز بها جميع الحالات. لقد حصلنا على اتحاد مميز ، على سبيل المثال ، يمكننا أن نفهم ما في الحمولة.

تحديث كود Redux الأصلي:

export const taskReducer = (state: State, action: Actions): State = > {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, error: action.payload }
    }
    
    return state
}

هناك خطر هنا إذا كتبنا string...
type FetchAction = {
    type: string
}
... ثم سينكسر كل شيء ، لأنه لم يعد تمييزًا.

في هذا الإجراء ، يمكن أن يكون النوع أي سلسلة على الإطلاق ، وليس تمييزًا محددًا. بعد ذلك ، لن يتمكن TypeScript من تمييز إجراء واحد عن آخر والعثور على الأخطاء. لذلك ، يجب أن يكون هناك حرفًا سلسلة حرفية تمامًا. وعلاوة على ذلك، يمكننا أن نضيف هذا التصميم: type ActionType = Actions["type"].

ستظهر ثلاثة من خياراتنا في المطالبات.



إذا كتبت ...
type FetchAction = {
    type: string
}
... ستكون المطالبات بسيطة string، لأن جميع الخطوط الأخرى لم تعد مهمة.



لقد كتبنا جميعًا وحصلنا على نوع المبلغ.

فحص شامل


تخيل موقفًا افتراضيًا حيث لا تتم إضافة معالجة الخطأ إلى التعليمات البرمجية.

export const taskReducer = (state: State, action: Actions): State = > {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        //case "TASK_FAIL":
            //return { isFetching: false, error: action.payload }
    }
    
    return state
}

هنا سيساعدنا الفحص الشامل - خاصية مفيدة أخرى مثل المبلغ . هذه فرصة للتحقق من أننا قد عالجنا جميع الحالات الممكنة.

إضافة متغير بعد بيان التبديل: const exhaustiveCheck: never = action.

أبدا نوع مثير للاهتمام:

  • إذا كانت نتيجة إرجاع الوظيفة ، فلن تنتهي الوظيفة بشكل صحيح أبدًا (على سبيل المثال ، دائمًا ما تخطئ أو يتم تنفيذها بلا نهاية) ؛
  • إذا كان نوعًا ، فلا يمكن إنشاء هذا النوع دون أي جهد إضافي.

سيشير المحول البرمجي الآن إلى الخطأ "النوع" FailAction "غير قابل للتعيين على النوع" مطلقًا ". لدينا ثلاثة أنواع من الإجراءات التي لم نعالجها "TASK_FAIL"- هذا هو FailAction. ولكن إلى الأبد ، فإنه ليس "قابلاً للتنازل".

دعنا نضيف معالجة "TASK_FAIL"، ولن يكون هناك المزيد من الأخطاء. تمت معالجته "TASK_FETCH"- تم إرجاعه ، معالجته "TASK_SUCCESS"- تم إرجاعه ، معالجته "TASK_FAIL". عندما قمنا بمعالجة جميع الوظائف الثلاث ، ماذا يمكن أن يكون الإجراء؟ لا شيء - never.

إذا قمت بإضافة إجراء آخر ، سيخبرك المترجم بالإجراءات التي لم تتم معالجتها. سيساعدك هذا إذا كنت ترغب في الرد على جميع الإجراءات ، وإذا كان ذلك انتقائيًا فقط ، فلا.

النصيحة الأولى

ابذل جهدك.
في البداية ، سيكون من الصعب الغوص: اقرأ عن نوع المجموع ، ثم عن المنتجات وأنواع البيانات الجبرية وما إلى ذلك في السلسلة. سوف تضطر إلى بذل جهد.

عندما نتعمق أكثر ، يزداد ضغط عمود الماء فوقنا. على عمق معين ، تتوازن قوة الطفو وضغط الماء فوقنا. هذه منطقة "طفو محايد" ، حيث نحن في حالة انعدام الوزن. في هذه الحالة ، يمكننا التحول إلى أنظمة أو لغات أخرى.

نظام النوع الاسمي


في روما القديمة ، كان لدى السكان ثلاثة مكونات للاسم: اللقب والاسم و "اللقب". الاسم نفسه هو "اسم". من هذا يأتي نظام النوع الاسمي - "الاسمي". من المفيد في بعض الأحيان.

على سبيل المثال ، لدينا مثل رمز الوظيفة.

export const githubUrl = process.env.GITHUB_URL as string
export const nodeEnv = process.env.NODE_ENV as string

export const fetchStarredRepos = (
    nodeEnv: string,
    githubUrl: string
): Promise<GithubRepo[ ]> => {
    if (nodeEnv = "production") {
        // log call
    }

    // prettier-ignore
    return fetch('$githubUrl}/users/saitonakamura/starred')
        .then(r => r.json());
}

ويأتي هذا التكوين على API: GITHUB_URL=https://api.github.com. githubUrlتسحب طريقة API المستودعات التي نضع عليها علامة النجمة. وإذا كانت nodeEnv = "production"تسجل هذه المكالمة ، على سبيل المثال ، للمقاييس.

للوظيفة التي نريد تطويرها (UI).

import React, { useState } from "react"

export const StarredRepos = () => {
    const [starredRepos, setStarredRepos] = useState<GithubRepo[ ] | null>(null)

    if (!starredRepos) return <div>Loading…</div>

    return (
        <ul>
            {starredRepos.map(repo => (
                <li key={repo.name}>repo.name}</li>
            ))}
        </ul>
    )
}

type GithubRepo = {
    name: string
}

الوظيفة تعرف بالفعل كيفية عرض البيانات ، وإذا لم يكن هناك شيء ، فعندها محمل. يبقى لإضافة مكالمة API وملء البيانات.

useEffect(() => {
    fetchStarredRepos(githubUrl, nodeEnv).then(data => setStarredRepos(data))
}, [ ])

ولكن إذا قمنا بتشغيل هذا الرمز ، فسوف يسقط كل شيء. في لوحة المطورين ، نجد أنه يتم fetchالوصول إلى العنوان '/users/saitonakamura/starred'- اختفى githubUrl في مكان ما. اتضح أن كل ذلك بسبب التصميم الغريب - nodeEnvيأتي أولاً. بالطبع ، قد يكون من المغري تغيير كل شيء ، ولكن يمكن استخدام الوظيفة في أماكن أخرى في قاعدة التعليمات البرمجية.

ولكن ماذا لو طالب المترجم بذلك مقدمًا؟ بعد ذلك ، لست مضطرًا لخوض دورة الإطلاق بأكملها أو اكتشاف خطأ أو البحث عن الأسباب.

العلامة التجارية


يحتوي TypeScript على اختراق لهذا النوع من العلامات التجارية. إنشاء Brandو B(سلسلة) Tونوعين. سننشئ نفس النوع لـ NodeEnv.

type Brand<T, B extends string> = T & { readonly _brand: B }

type GithubUrl = Brand<string, "githubUrl">
export const githubUrl = process.env.GITHUB_URL as GithubUrl

type NodeEnv = Brand<string, "nodeEnv">
export const nodeEnv = process.env.NODE_ENV as NodeEnv

لكن حصلنا على خطأ. من المستحيل



الآن تعيين بعضها البعض ، لأن هذه هي أنواع رمزية. غير مزعجة ، لكنها اسمية. الآن لا يمكننا تبديلها هنا - نغيرها في جزء آخر من الشفرة.githubUrlnodeEnv

useEffect(() => {
    fetchStarresRepos(nodeEnv, githubUrl).then(data => setStarredRepos(data))
}, [ ])

الآن كل شيء على ما يرام - لقد حصلوا على بدائية ذات علامة تجارية . تُعد العلامة التجارية مفيدة عند مواجهة عدة وسيطات (سلاسل ، أرقام). لديهم دلالات معينة (إحداثيات س ، ص) ، ويجب عدم الخلط بينها. انها مريحة عندما يخبرك المترجم أنهم مرتبكون.

ولكن هناك نوعان من المشاكل. أولاً ، ليس لدى TypeScript أنواع اسمية أصلية. ولكن هناك أمل في أن يتم حلها ، ولا تزال مناقشة هذه المسألة جارية في مستودع اللغة .

المشكلة الثانية هي "as" ، لا توجد ضمانات GITHUB_URLبعدم كسر الارتباط.

وكذلك مع NODE_ENV. على الأرجح ، لا نريد بعض الخيوط فقط ، ولكن "production"أو "development".

type NodeEnv = Brand<"production" | "development" | "nodeEnv">
export const nodeEnv = process.env.NODE_ENV as NodeEnv

كل هذا يحتاج إلى التحقق. أنا أحيلك إلى المصممين الأذكياء وتقرير سيرغي تشيريبانوف " تصميم مجال باستخدام TypeScript بأسلوب وظيفي ".

النصائح الثانية والثالثة

كن حذرا.
في بعض الأحيان تتوقف وتنظر حولها: في لغات وأطر وأنظمة كتابة أخرى. تعلم مبادئ جديدة وتعلم الدروس.

عندما نتجاوز نقطة "الطفو المحايد" ، يضغط الماء بقوة ويسحبنا للأسفل.
الاسترخاء.
تعمق أكثر ودع TypeScript يقوم بعمله.

ما الذي يمكن لـ TypeScript فعله


يمكن لـ TypeScript طباعة أنواع .

export const createFetch = () => ({
    type: "TASK_FETCH"
})

export const createSuccess = (task: Task) => ({
    type: "TASK_SUCCESS"
    payload: Task
})

export const createFail = (error: Error) => ({
    type: "TASK_FAIL"
    payload: error
})

type FetchAction = {
    type: "TASK_FETCH",

type SuccessAction = {
    type: "TASK_SUCCESS",
    payload: Task

type FailAction = {
    type: "TASK_FAIL",
    payload: Error
}

يوجد TypeScript لهذا ReturnType- يحصل على القيمة المرجعة من الوظيفة:
type FetchAction = ReturnType<typeof createFetch>
نمر فيه نوع الوظيفة. لا يمكننا ببساطة كتابة دالة: لأخذ نوع من دالة أو متغير ، نحتاج إلى الكتابة typeof.

نرى في النصائح type: string.



هذا أمر سيئ - سينفصل المميّز بسبب وجود شيء حرفي.

export const createFetch = () => ({
    type: "TASK_FETCH"
})

عندما نقوم بإنشاء كائن في جافا سكريبت ، فإنه قابل للتغيير بشكل افتراضي. هذا يعني أنه في كائن يحتوي على حقل وسلسلة ، يمكننا فيما بعد تغيير السلسلة إلى أخرى. لذلك ، يمتد TypeScript سلسلة معينة إلى أي سلسلة للكائنات القابلة للتغيير.

نحن بحاجة إلى مساعدة TypeScript بطريقة أو بأخرى. هناك ثوابت لهذا.

export const createFetch = ( ) => ({
    type: "TASK_FETCH" as const
})

الإضافة - ستختفي السلسلة فورًا في المطالبات. يمكننا كتابة هذا ليس فقط عبر الخط ، ولكن بشكل عام الحرفي بأكمله.

export const createFetch = ( ) => ({
    type: "TASK_FETCH"
} as const)

ثم يصبح النوع (وجميع الحقول) للقراءة فقط.

type FetchAction = {
    readonly type: "TASK_FETCH";
}

هذا مفيد لأنه من غير المحتمل أن تغير قابلية تصرفك. لذلك ، نضيف كعناصر ثابتة في كل مكان.

export const createFetch = () => ({
    type: "TASK_FETCH"
} as const)

export const createSuccess = (task: Task) => ({
    type: "TASK_SUCCESS"
    payload: Task
} as const)

export const createFail = (error: Error) => ({
    type: "TASK_FAIL"
    payload: error
} as const)

type Actions =
    | ReturnType<typeof createFetch>
    | ReturnType<typeof createSuccess>
    | ReturnType<typeof createFail>

type State =
    | { isFetching: true }
    | { isFetching: false; task: Task }
    | { isFetching: false; error: Error }

export const taskReducer = (state: State, action: Actions): State = > {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, error: action.payload }
}

  const _exhaustiveCheck: never = action

  return state
}

تم تقليل جميع إجراءات كتابة التعليمات البرمجية وإضافتها كنوع. فهم TypeScript كل شيء آخر.

يمكن لـ TypeScript إخراج الحالة . يتم تمثيله بواسطة النقابة في الرمز أعلاه مع ثلاث حالات محتملة لـ isFetching: true أو false أو Task.

استخدم type state = ReturnType. تشير مطالبات TypeScript إلى وجود تبعية دائرية.



تقصر.

type State = ReturnType<typeof taskReducer>

export const taskReducer = (state, action: Actions) => {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, error: action.payload }
}

const _exhaustiveCheck: never = action

return state
}

Stateتوقف عن الشتم ، لكنه الآن any، لأن لدينا اعتماد دوري. نكتب الحجة.

type State = ReturnType<typeof taskReducer>

export const taskReducer = (state: { isFetching: true }, action: Actions) => {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, error: action.payload }
}

const _exhaustiveCheck: never = action

return state
}

الاستنتاج جاهز.



والاستنتاج هو على غرار ما كان لدينا في الأصل: true، false، Task. هناك حقول قمامة هنا Error، ولكن مع النوع undefined- يبدو أن الحقل موجود ، ولكن لا يبدو.

النصيحة الرابعة

لا تبالغ.
إذا كنت تسترخي وتغوص بعمق شديد ، فقد لا يكون هناك ما يكفي من الأكسجين للعودة.

التدريب أيضًا: إذا كنت منغمسًا جدًا في التكنولوجيا وقررت تطبيقه في كل مكان ، فعلى الأرجح ستواجه أخطاء ، والأسباب التي لا تعرف سببها. سيؤدي هذا إلى الرفض ولن يرغب في استخدام أنواع ثابتة بعد الآن. حاول تقييم قوتك.

كيف يبطئ TypeScript من التنمية


سيستغرق بعض الوقت لدعم الكتابة. لا يحتوي على أفضل تجربة مستخدم - في بعض الأحيان يعطي أخطاء غير مفهومة تمامًا ، مثل أي نظام نوع آخر ، مثل Flow أو Haskell.
كلما كان النظام أكثر تعبيراً ، كان الخطأ أكثر صعوبة.
تتمثل قيمة نظام النوع في أنه يوفر تعليقات سريعة عند حدوث أخطاء. سيعرض النظام الأخطاء وسيستغرق وقتًا أقل للعثور عليها وتصحيحها. إذا كنت تقضي وقتًا أقل في تصحيح الأخطاء ، فستتلقى المزيد من الحلول المعمارية مزيدًا من الاهتمام. لا تبطئ الأنواع من التطور إذا تعلمت العمل معها.

++. - , , (25 26 ) - (27 — 10 ).

(5900 ), ++ IT-, .

AvitoTech FrontendConf 2019 . youtube- telegram @FrontendConfChannel, .

All Articles