Vue3 + TypeScript 实践总结

为什么 TypeScript

Vue2.x 对 TypeScript 的支持是硬伤,而 TypeScript 对于大型项目更加友好,团队协作时强类型总比约定更严谨。所以,一些大型工程在技术选型时,抛弃 Vue 而选择 React。尤其是在 React16.8+ 函数式组件有状态以后。

为什么会出现 setup

首先,Vue 只是 MVC 中的 View 层,需要将 Model、Controller 分离开才能最大化地降低耦合、增加复用。即Vue 单文件组件应该只是处理视图层的业务逻辑及渲染即可,而 Controller 的业务逻辑不应该在单文件组件中,因此诞生了 Vuex,而 Vuex 更多是一个全局状态管理,在处理一些业务逻辑、数据交互、组件通信上,使用 Vuex 来管理显得太重了。因而在 View 与 Controller 的业务逻辑处理上,有了一个真空地带,导致我们在 methods 中耦合了大量的业务逻辑,随着版本不断迭代,一旦体量变大,公共方法的抽离、代码维护的成本指数倍增长,组件已变得不可复用,难以阅读,能做的只能继续往上面堆;

其次,一些需要响应式的公共属性、方法,我们通常引入 mixin 来隔离,但是 mixin 不太好管理,容易污染。

不难发现,业务逻辑耦合度变高后,最终导致 Vue 单文件组件的作为 View 层,页面 UI 哪怕相似度极高,也可能无法复用。

高内聚、低耦合,对于 Vue 来说,即需要能够分离业务逻辑关注点、易管理、响应式的API,setup 为了这而产生。

Vue 实践

Props 自身的 type 属性无法描述更复杂的数据结构,TS 会报错

Vue 提供了类型别名 PropType,用于定义接收的更复杂的 props 属性结构。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n16" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">import { PropType, defineComponent, RendererElement } from 'vue';

interface ITabItem {
text: string,
id: string | number,
active?: boolean
}

const XComponent = defineComponent({
props: {
tabList: {
type: Array as PropType<Array<ITabItem>>,
default: () => []
}
},
setup() {
return (): RendererElement => (
<div>{
props.tabList.map(tab => {
return <p key={tab.id}>{tab.text}</p>
})
}</div>
)
}
})</pre>

单文件 vue 组件与 tsx 函数组件间的 slot 使用

单文件 vue 组件互相之间的 slot 使用,可阅读官方文档 单文件插槽

jsx/tsx 组件互相之间的 slot 使用,可以阅读文档 jsx/tsx 插槽

单文件同 jsx、tsx 组件之间的 slot 使用,示例如下:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="tsx" cid="n22" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">// vue 单文件中引入 tsx 函数式组件 XInput
<section>
<x-input v-model="inputInfo.value">
<template v-slot:search>
<p>This is search slot.</p>
</templatet>
</x-input>
</section>

// XInput
const SearchBox = (props: IChildProp) => {
const value = props.value;
const clickEv = () => {
// todo
props.clickHandler?.();
}
return (
<div class="search-box" onClick={clickEv}>
<img src={searchIcon} />
</div>
)
}
export default defineComponent({
setup(props, { emit, slots }) {
const value = ref(props.value);
const inputEv = (ev: Event) => {
value.value = (ev.target as HTMLInputElement).value;
emit('update:input', value);
}
const searchEv = ():void => {
// todo
}
return (): RendererElement => (
<div class="x-input">
<input class="input-el" value={value.value} onInput={inputEv} />
{
slots.search &&
(<>
<SearchBox clickHandler={searchEv} value={value.value} />
{slot.search?.()} // 渲染成 <p>This is a search slot.</p>
</>)
}
</div>
)
}
})</pre>

自定义hooks——将“动作”转成 “状态”

// todo

巧用类型收缩解决 TypeScript 报错

1、类型断言

类型断言可以明确告诉 TypeScript 值的详细类型,在某些场景,我们非常确认它的类型,即使与 typescript 推断出来的类型不一致。

2、类型守卫

  • typeof: 用于判断 number, string, boolean 或 symbol 四种类型

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n36" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">function padLeft(value: string | number) {
if (typeof value === string) {
console.log(value.length);
}
}</pre>

  • instanceof: 用于判断一个实例是否属于某个类

    <pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n40" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">class Man {
    handsome = 'handsome'
    }
    class Woman {
    beautiful = 'beautiful'
    }
    function Human(arg: Man | Woman) {
    if (arg instanceof Man) {
    console.log(arg.handsome);
    } else {
    console.log(arg.beautiful);
    }
    }</pre>

  • in: 用于判断一个属性/方法是否属于某个对象

    <pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n43" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">interface B {
    b: string
    }
    interface A {
    a: string
    }
    function foo(x: A | B) {
    if ('a' in x) {
    return x.a;
    }
    return x.b;
    }</pre>

  • 字面量类型保护

    有些场景,使用 in, instanceof, typeof 太过麻烦。这时候可以自己构造一个字面量类型

    <pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n48" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">type Man = {
    handsome: 'handsome';
    type: 'man';
    }
    type Woman = {
    beautiful: 'beautiful';
    type: 'woman';
    }
    function Human(arg: Man | Woman) {
    if (arg.type === 'man') {
    console.log(arg.handsome);
    } else {
    console.log(arg.beautiful);
    }
    }</pre>

3、双重断言

有些时候使用 as 也会报错,因为 as 断言的时候也不是毫无条件的。它只有当 S 类型是 T 类型的子集时,S能被断言成T。

所以面对这样的情况,只想暴力解决问题,可以使用双重断言,首先断言成兼容所有类型的 unknown。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n54" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">function handler(event: Event) {
const element = event as HTMLElement; // Error
// 'Event' 和 'HTMLElement' 中的任何一个都不能复制给另一个
}

// 首先断言成 unknown
function handler(event: Event) {
const element = (event as unknown) as HTMLElement;
}</pre>

4、用户自定义的类型保护

假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道它的类型就好了。 TypeScript 里的类型保护机制使其成为现实。

类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n60" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}</pre>

在这个例子里,pet is Fish 就是类型谓词。谓词为 parameterName is Type 这种形式,parameterName 必须是来自当前函数签名里的一个参数名。

当使用一些变量调用 isFish 时,TypeScript 会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n63" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">// TypeScript 不仅知道在 if 分支里 pet 是 Fish 类型;还清楚在 else 分支里,一定不是 Fish 类型,一定是 Bird 类型。
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}</pre>

5、使用 never 收窄类型

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n66" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">interface Foo {
type: 'foo'
}

interface Bar {
type: 'bar'
}
// 一个联合类型 All
type All = Foo | Bar;

function handleValue(val: All) {
switch (val.type) {
case 'foo':
break;
case 'bar':
break;
default:
// val 在这里是 never
const exhaustiveCheck: never = val;
break;
}
}</pre>

注意在 default 里面我们把收窄为 never 的 val 赋值给了一个现实声明为 never 的变量。如果一切逻辑正常,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All 的类型:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n69" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">type All = Foo | Bar | Baz</pre>

然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,这个时候 default 里面 val 会被收窄为 Baz,导致无法复制给 never,产生一个编译错误。所以通过这个办法,可以确保 handleValue 总是穷尽了所有的 All 的可能类型。

6、非空断言运算符!

这个运算符可以用在变量名或者函数名之后,用来强调对应的元素是 非 null|undefined 的,应用场景,特别适合我们已经明确知道不会返回空值的场景,从而减少冗余的代码判断。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n75" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">this.$refs.container!.scrollWidth</pre>

7、键值获取 keyof

keyof 可以获取一个类型所有键值,返回一个联合类型

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n79" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">type Person = {
name: string;
age: number;
}
type PersonKey = keyof Person; // 'name' | 'age'</pre>

keyof 的一个典型应用场景是限制访问对象的 key 合法化,因为 any 做索引是不被接收的

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n81" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">function getValue (p: Person, k: keyof Person) {
return p[k];
}</pre>

巧用高级类型灵活处理数据

1、类型索引

使用索引类型,编译器就能够检查使用了动态属性名的代码。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n89" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}

interface Person {
name: string;
age: number;
}
let person: Person {
name: 'Jack',
age: 18
}

let strings: string[] = pluck(person, ['name']);
</pre>

编译器会检查 name 是否真的是 Person 的一个属性。

keyof T 索引类型查询操作符。对于任何类型 T,keyof T 的结果为 T 上已知的公共属性名的联合。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n93" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">let personProps: keyof Person; // 'name' | 'age';</pre>

T[K] 索引访问操作符。在这里,类型语法反映了表达式语法。就像索引类型查询符一样,可以在普通的上下文中使用 T[K],只要确保类型变量 K extends keyof T 就可以了。

  • keyof: 获取类型上的 key 值

  • extends: 泛型里面的约束

  • T[P]: 获取对象 T 相应 K 的元素类型

    <pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n103" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">type Partial<T> = {
    [P in keyof T]?: T[P]
    }
    // [自定义变量名 in 枚举类型]: 类型</pre>

在使用 props 的时候,有时候全部属性都是可选的,如果一个一个属性写 ?,大量的重复动作,这种时候可以直接使用 Partial<State>

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n108" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">// 如何用 ts 声明 AnimalMap?
const AnimalMap = {
cat: { name: '猫', title: 'cat' },
dog: { name: '狗', title: 'dog' },
frog: { name: '蛙', title: 'wa' }
}

// Record 第一个泛型传入对象的 key 值,第二个传入对象的属性
type Record<K extends string, T> = {
[P in K]: T;
}

type AnimalType = 'cat' | 'dog' | 'frog';
interface AnimalDescription {
name: string;
title: string
}

const AnimalMap: Record<AnimalType, AnimalDescription> {
cat: { name: '猫', title: 'cat' },
dog: { name: '狗', title: 'dog' },
frog: { name: '蛙', title: 'wa' }
}</pre>

2、可辨识联合

合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合 的高级模式,它也称作 标签联合代数数据 数据类型。可辨识联合在函数式编程中很有用处。

三要素:

  • 具有普通的单利类型属性 —— 可辨识的特征

  • 一个类型别名包含了那些类型的联合 —— 联合

  • 此属性上的类型保护

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n121" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">interface Square {
kind: 'square';
size: number
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
interface Circle {
kind: 'circle';
radius: number;
}

type Shape = Square | Rectangle | Circle;

function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}

// 可辨识联合
function area(s: Shape) {
switch (s.kind) {
case 'square':
return s.size * s.size;
case 'rectangle':
return s.height * s.width;
case 'circle':
return Math.PI * s.radius ** 2;
default:
return assertNever(s); // 处理遗漏的 case
}
}</pre>

assertNever 检查 s 是否为 never 类型 —— 即为出去所有可能情况后剩下的类型。

装饰器(Decorator)的理解

装饰器(Decorator) 是一种特殊类型的声明,它能够被附加到类声明方法访问符属性参数 上。装饰器使用 @expression 这种形式,expression 求值后必须为一个函数,它会在运行时被调用,被装饰声明信息作为参数传入,它添加额外的方法或属性到基类上。为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。

我们简单的理解装饰器,可以认为它是一种包装,对对象、方法、属性的包装。当我们需要访问一个对象的时候,如果我们通过这个对象外围的包装去访问的话,被这个包装附加的行为就会被触发。例如一把加了消声器的枪,消声器就是一个装饰器,但是它和原来的枪成为一个整体,开枪的时候消声器就会发生作用。

装饰器组合

多个装饰器可以同时应用到一个声明上:

  • 书写在同一行上:

    <pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n136" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">@f @g x</pre>

  • 书写在多行上(常用):

    <pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n139" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">@f
    @g
    x</pre>

当多个装饰器应用于一个声明上,它们求值方式与 复合函数 相似。

当多个装饰器应用在一个声明上时会进行如下步骤的操作(调用栈,后进先出):

1、由上至下依次对装饰器表达式求值

2、求值的结果会被当作函数,由下至上依次调用

leDecorator是welcome函数的装饰器:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n147" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">function leDecorator(target, propertyKey: string, descriptor: PropertyDescriptor): any {
console.log('enter decorator');
const oldValue = descriptor.value;
descriptor.value = function() {
console.log(Calling "${propertyKey} with", arguments, target, target instanceof Object ,descriptor);
const value = oldValue.apply(null, [arguments[1], arguments[0]]);
console.log('Function is executed');
return ${value}; This is awesome;
};
return descriptor;
}

class JSMeetup {
public speaker = 'Ruban';
// @leDecorator
welcome(arg1, arg2) {
console.log(Arguments Received are ${arg1} ${arg2});
return ${arg1} ${arg2};
}
}

const meetup = new JSMeetup();
console.log(meetup.welcome('World', 'Hello'));

// 注释掉 @leDecorator
/*
Arguments Received are World Hello
World Hello
*/

// 放开注释
/*
enter decorator
Calling "welcome" with [Arguments] { '0': 'World', '1': 'Hello' } {}, true, {
value: [Function (anonymous)],
writable: true,
enumerable: false,
configuarble: true
}
Arguments Received are Hello World
Function is executed
Hello World; This is awesome
*/ </pre>

TypeScript 中共计有 5 类装饰器:

  • 方法装饰器

  • 属性装饰器

  • 类装饰器

  • 参数装饰器

  • 访问器装饰器

方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。它会被应用到方法的属性描述符上,可以用来监视、修改或者替换方法定义。方法装饰器不能用在声明文件(*.d.ts),重载或者任何外部上下文(比如 declare 的类)中。

下面是方法装饰器的定义:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n165" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">MethodDecorator = <T>(target: object, key: string, decriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;</pre>

参数:

  • target: 对于静态成员来说是类的构造函数(construcor),对于实例成员是类的原型对象(Object)

  • key: 成员的名字(被装饰的函数名)

  • descriptor: 成员(被装饰的函数)的属性描述符

下面是一个方法装饰器(@writable)的例子,应用于 Greeter 类的方法上:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n175" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">function writable(value: boolean) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.writable = value;
return descriptor;
}
}

class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
// @writable(false)
greet() {
return Hello, ${this.greeting};
}
}

const greeter = new Greeter('Tony');
greeter.greet = () => { return 'Hello, Jack' };
console.log(greeter.greet());

// 注释修饰器writable
// Hello, Jack

// 放开注释
// Hello, Tony</pre>

这里的 @writable(false) 是一个 装饰器工厂 。当装饰器 @writable(false) 被调用时,它会修改属性描述符的 writable 属性。

注意点:

  • 装饰器在 class 被声明的时候执行,而不是class实例化的时候

  • 方法装饰器返回一个值

  • 存储原有的描述符并且返回一个新的描述符是推荐的做法。这在多描述符应用场景下非常有用

  • 设置描述符value的时候,不要使用箭头函数

属性装饰器

通过属性装饰器,可以重新定义 getter, setter, 修改 enumerable, writable, configurable 等属性,也可以用来记录这个属性的元数据。

属性装饰器定义如下:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n193" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">PropertyDecorator = (target: object, key: string) => void;</pre>

属性装饰器表达式会在运行时当作函数调用,传入下列2个参数

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象,即属性拥有者

  • 成员的名字,属性名

用 Object.defineProperty 来实现一个简单的属性装饰器

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n203" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">function realName(target, key: string): any {
let _val = target[key];

const getter = function() {
return ${_val} Ma;
}
const setter = function(newVal) {
_val = newVal;
}

Object.defineProperty(target, key, {
get: getter,
set: setter
})
}

class JSMeetup {
// @realName
public myName = 'Tony';
greet() {
return Hi, I'm ${this.myName};
}
}

const meetup = new JSMeetup();
console.log(meetup.greet());

// 注释装饰器realName
// Hi, I'm Tony

// 放开注释
// Hi, I'm Tony Ma</pre>

metadata(元数据)

描述数据的数据,也叫元数据。它是对一直心系的一种集合称谓,它可以是描述某项具体的数值,也可以是描述影像或声音的内容,也可能 只是一些注释。它们往往跟随着对象文件,让我们得以更全面的了解对象相关的信息。eg. 照片的元数据包括图像的尺寸、拍摄时间、光圈、快门、GPS等;视频文件画面尺寸、视频和音频的编码、时长等等。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n208" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">import 'reflect-metadata';

const formatMetadataKey = Symbol('format');

function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

class Greeter {
@format('Hello, %s')
greeting: string;

constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, 'greeting');
return formatString.replace('%s', this.greeting);
}
}

const greeter = new Greeter('Tony');
console.log(greeter.greet());
// Hello, Tony</pre>

这个 @format('Hello, $s') 装饰器是一个 装饰器工厂。被调用时,添加一条这个属性的元数据。当 getFormat 被调用时,它读取对应属性的元数据。

类装饰器

类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

类装饰器定义如下:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n216" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">ClassDecorator = <T extends Function>(target: T) => T</pre>

类装饰器表达式会在运行时当作函数调用,类的构造函数作为其唯一的参数。

无返回值:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n220" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">// 通过 Object.seal 密封此类的构造函数和原型
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}

// @sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
this.constructor.prototype.toString = function() {};
console.log(this.constructor.prototype);
}
greet() {
return Hello, ${this.greeting};
}
}

const greeter = new Greeter('Tony');
console.log(greeter.greet());

// 注释装饰器 sealed
// { toString: [Function (anonymous)] }
// Hello, Tony

// 取消注释
// TypeError: Cannot add property toString, object is not extensible</pre>

有返回值:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n222" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">interface Extra {
extra: string
}

function AwesomeMeetup<T extends { new (...args: any[]): {} }>(constructor: T): T {
return class extends constructor implements Extra {
extra = 'Tadah!';
speaker: string = 'Ragularuban';
hello: string = 'Hello';
}
}

// @AwesomeMeetup
class JSMeetup {
public speaker = 'Ruban';
public hello: string;
constructor(message: string) {
this.hello = message;
}
greet() {
return Hi, I'm ${this.speaker};
}
}

const meetup = new JSMeetup('Hi') as JSMeetup & Extra;
console.log(meetup.greet());
console.log(meetup.extra);

class A extends JSMeetup {}

const a = new A('HeHe');
console.log(a.greet());

// 注释装饰器 AwesomeMeetup
/*
Hi, I'm Ruban
undefined
HeHe, I'm Ruban
*/

// 取消注释
/*
Hello, I'm Ragularuban
Tadah!
Hello, I'm Ragularuban
*/</pre>

参数装饰器

参数装饰器应用于类构造函数或方法声明,往往用来对特殊的参数进行标记,然后在方法装饰器中读取对应的标记,执行进一步的操作。

参数装饰器往往搭配方法装饰器一起使用

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象

  • 成员的名字

  • 参数在函数参数列表中的索引

参数装饰器只能用来监视一个方法的参数是否被传入,参数装饰器的返回值会被忽略。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n237" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">function logParameter(target: any, key: string, index: number) {
const metadataKey = 'myMetaData';
if (Array.isArray(target[metadataKey])) {
target[metadataKey].push(index);
} else {
target[metadataKey] = [index];
}
console.log('param target: ', target);
}

function logMethod(target, key: string, descriptor: any): any {
const originalMethod = descriptor.value;
// 参数装饰器、方法装饰器的第一个参数 target 是同一个引用
console.log('method target: ', target);
descriptor.value = function(...args: any[]) {
const metadataKey = 'myMetaData';
const indices = target[metadataKey];
for (let i = 0; i < args.length; i++) {
if (indices.indexOf(i) !== -1) {
args[i] = 'Abrakadabra';
}
}
const result = originalMethod.apply(this, args);
return result;
}
return descriptor;
}

class JSMeetup {
// @logMethod
public saySomething(something: string, @logParameter somethingElse: string): string {
return ${something} : ${somethingElse};
}
}

const meetup = new JSMeetup();
console.log(meetup.saySomething('Something', 'Something Else'));

// 注释掉方法装饰器 logMethod
/*
param target: { myMetaData: [1] }
Something : Something Else
*/

// 取消注释
/*
param target: { myMetaData: [1] }
method target: { myMetaData: [1] }
Something : Abrakadabra
*/
</pre>

访问器装饰器

访问器装饰器应用于访问器的属性描述符,可以用来监视、修改或替换一个访问器的定义。

不能向多个同名的 get/set 访问器应用装饰器,get/set 只能选择其一应用

访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象

  • 成员的名字

  • 成员的属性描述符

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n251" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">function configurable(value: boolean) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// descriptor.writable = false 会报错后再赋值就会报错
descriptor.configurable = value;
}
}

class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}

@configurable(false)
get x() {
return this._x;
}

@configurable(false)
get y() {
return this._y;
}
}

function reWrite(value: number) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.get = function() {
return value;
}
return descriptor;
}
}

class Greeter {
_x: number;
_y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}

set x(value: number) {
this._x = value;
}

// @reWrite(999)
get x() {
return this._x;
}

getPoint() {
console.log(x: ${this.x}, y: ${this._y});
}
}

const greeter = new Greeter(100, 100);
greeter.getPoint();

// 注释掉访问器装饰器 reWrite
// x: 100, y: 100

// 去取消注释
// x: 999, y: 100</pre>

TypeScript 解决了哪些痛点

属性、方法等访问/调用错误,空指针等基于非预期类型的错误

常见错误:

  • Uncaught TypeError: Cannot read property

  • TypeError: 'undefined' is not an object

  • TypeError: null is not an object

  • TypeError: 'undefined' is not a function

  • TypeError: Cannot read property 'length'

  • ReferenceError: event is not defined

enum 枚举

常见的应用场景是描述某同一特征的常量、元数据,例如任务类型可能被定义为 '1', '2', '3',不同的任务类型有不同的处理逻辑,通过 switch/if 判断时,我们直接 if(taskType === '1') 是不可以的,枚举(enum) 的出现解决了这个问题,并且让代码的可阅读性更高。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n273" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">enum TaskType {
DOWNLOAD: '1',
SHARE: '2',
FEED: '3'
}
switch (type) {
case DOWNLOAD:
// todo
break;
// ...
}

enum ResStatus {
EXPECT: '1001',
ACT_OVER: '3001',
NO_LOGIN: '3010'
}</pre>

装饰器

一些底层的 Class 不能过多的业务逻辑或者耦合,或者在某些场景中,需要对元数据进行操作(元编程),或在真实应用中,不能尽善尽美,装饰器可以看做是一种润滑剂、补丁、对元数据的操作(元编程),或者说可以称之为面向切面编程(AOP)。

泛型

TypeScript 高级用法(字节前端)

泛型在 TS 中承载了从静态定义到动态调用的桥梁,同时也是 TS 对自己类型定义的元编程。

基本使用

泛型可以用在普通类型定义,类定义、函数定义上:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n285" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">// 普通类型定义
type Dog<T> = { name: string, type: T }
// 普通类型使用
// 需要把泛型类型也写上去
const dog: Dog<number> = { name: 'wang', type: 3 }

// 类定义
class Cat<T> {
private type: T;
constructor(type: T) {
this.type = type;
}
}
// 类使用
// 变量能够推断出来,可以省略泛型书写
const cat: Cat<number> = new Cat<number>(20); // const cat = new Cat(20);

// 函数定义
function swipe<T, U>(value: [T, U]): [U, T] {
return [value[1], value[0]];
}
// 函数使用
// 变量能够推断出来,可以省略泛型书写
swipe<Cat<number>, Dog<number>>([cat, dog]); // swipe([cat, dog]);</pre>

泛型约束

有的时候,可以不用关注泛型具体的类型

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n289" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">function fill<T>(length: number, value: T): T[] {
return new Array(length).fill(value);
}</pre>

有时候,需要限定类型,可以使用关键字 extends

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n291" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">function sum<T extends number>(value: T[]): number {
let count = 0;
value.forEach(v => count += v);
return count;
}</pre>

这样可以 sum([1, 2, 3]) 的方式调用,而 sum(['1', '2', '3']) 则无法通过编译。

泛型推断 infer

一般搭配泛型条件语句使用,所谓推断,就是不用预先指定在泛型列表中,在运行时会自动判断,不过先得预先定义好整体的结构。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n296" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">type Foo<T> = T extends {t: infer Test} ? Test: string;</pre>

首先看 extends 后面的类容,{t: infer Test} 可以看成一个包含 t 属性的类型定义,这个 t 属性的 value 类型通过 infer 进行推断后会赋值给 Test 类型,如果泛型实际参数符合 {t: infer Test} 的定义,那么返回 Test 类型,否则默认返回 string 类型。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n298" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">// 返回 string,因为 number 不是一个包含 t 属性的对象类型
type One = Foo<number>;

// 返回 boolean,因为泛型参数中有 t 属性的对象类型,使用了 infer 对应的类型,即 boolean
type Two = Foo<{t: boolean}>

// 同理,返回一个函数类型,() => void
type Three = Foo<{a: numbem, t: () => void}></pre>

Tips

  • Vue 提供 interface RendererElement,可用于 渲染函数/tsx 中作为返回值校验

  • 联合类型中巧用 never 类型,可以处理一些遗漏的 case

  • interface HTMLInputElement 的 value 属性获取输入框的值 (event.target as HTMLInputElement).value

  • 数据结构过于复杂、嵌套太深,类型判断无法推断时,可以试着使用 ?. ?.() obj as IObject 等方式解决

  • usehooks 中如果涉及到元素获取,最好使用 vue 单文件组件,ref 可以指向唯一值,tsx 等渲染函数中无法通过 ref 定位元素,当组件复用高时,通过 document.querySelector 拿到的不一定是预期元素(可以通过唯一 id 或 props 来解决)

  • setup 方法不能 async

  • 数组是一串相同类型的数据的集合,内存中连续存放,实际上存放的是地址,指向对象实例,JS 中数组并不是真正意义上的数组,故 TypeScript 中的定义数组时,需要指定数组元素中的类型

    <pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="javascript" cid="n318" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">// js 的数组可以是任意类型
    const hunmanList = [18, 'Tony', { like: 'Reading' }]</pre>

    <pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n320" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">interface Human {
    age: number,
    name: string
    }

    const humanList: Human[]
    const humanList: Array<Human></pre>

  • defineAsyncComponent 用于引用子组件处,接收的参数是一个 Promise<Component>

    <pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n323" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">const BannerDownload = defineAsyncComponent((): Promise<Component> => import('@/components/common/BannerDownload.vue'));</pre>

  • 通 react 一样,类型断言最好使用 el as HTMLElement 而非尖括号 <HTMLElement>el

  • Vue3 暴露的 interface, type 在文件 @vue/runtime-core/dist/runtime-core.d.ts

  • 当提示不能对 null 进行类型断言时,先对变量类型签名 unknown,使用 unknown 替代 any,既灵活又可以保证类型安全

    • Vue3 setup 中获取模板引用 ref,需要 return,示例如下:

    <pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="typescript" cid="n333" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">onMounted(() => {
    const videoBox = ref(null);
    onMounted(() => {
    const el: unknown = videoBox.value;
    console.log(window.getComputedStyle(el as HTMLElement).width);
    })
    return { videoBox }
    })</pre>

  • TypeScript 内置类型在 node_modules/typescript/lib 下的 *.d.ts 文件中

  • 全局安装 ts-node 然后在 .ts 文件中第一行 *#!/usr/bin/env ts-node* 终端中输入路径即刻直接执行 ts 文件

  • 构造函数的类型可以通过 { new (...args: any[]): {} } 来描述

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,406评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,976评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,302评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,366评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,372评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,457评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,872评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,521评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,717评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,523评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,590评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,299评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,859评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,883评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,127评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,760评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,290评论 2 342

推荐阅读更多精彩内容