前言
能点开这篇文章的人,多少都有点ts 基础,当哩个当,当哩个当,闲言碎语不要讲,就不介绍 ts 带来的优势了
总之一句话,js 不能提示的 ts 能提示, js 不能静态发现的错误 ts 能发现,js 能干的 ts 能干,js 不能干的 ts 也能干 !
通用项目
注释
最基础 但最重要
使用 ts 注释 可以很轻松的表达出每个属性更具体的含义,特别是对于相似的属性来说,往往更有意义
/** 这是一个人 */
interface Person {
/** This is name.表示一个人的名字 */
name: string,
// md 格式
/** - 一个人的年龄 */
age: number,
/**
* @property {object} address - 一个人的地址
* @property {string} address.city - 城市
* @property {string} address.town - 镇子
*/
address: {
city: string,
town: string
}
}
let m: Person = {
name: 'zs',
age: 26,
address: {
city: "",
town: ""
}
}
函数上定义其他属性
由于 js 中 函数可以当做 对象 使用,函数上是可以定义属性的
interface Counter {
():void,
count:number
}
// 如果使用 let 定义 会报错
// let c:Counter = () =>{}
const c:Counter = () =>{}
c.count = 12
使用 let 定义 回报下面 👇 的这个错误
const关键字确保不会发生对变量进行重新分配,并且只保证该字面量的严格类型
let定义的是可变的let a:string = 'str'那么 a的类型 是 string
如果用const定义,那么 a 的类型 是 str字面量
let扩展为更通用的类型,并允许将其重新分配给该类型的其他值
个人看法,如有不同,欢迎讨论
分发
🎈 在
css中,margin-left和margin-right可以合并成margin-inlinemargin-top和margin-bottom可以合并成margin-block
同理 的padding也可以合并为padding-inliine和padding-block看这里👉 mdn-margin-inline
看这里👉 mdn-padding-inline
type Contact<S1 extends string,S2 extends string> = `${S1}-${S2}`
type F = Contact<'top'|'bottom',"left" | "right"> // 具有分发作用
那什么时候会有分发?🧐
需要满足以下的条件
- 泛型传入
- 裸类型传入
详细可以看这里的文档 👉 条件类型的分布式特性文档
在条件类型中,如果被检查的类型是一个 “裸” 类型参数,即没有被数组、元组或 Promise 等包装过,则该条件类型被称为分布式条件类型即裸类型。
对于分布式条件类型来说,当传入的被检查类型是联合类型的话,在运算过程中会被分解成多个分支
// 分发
type Naked<T> = T extends boolean ? "Y" : "N";
// 非裸类型
type WrappedTuple<T> = [T] extends [boolean] ? "Y" : "N";
type WrappedArray<T> = T[] extends boolean[] ? "Y" : "N";
type WrappedPromise<T> = Promise<T> extends Promise<boolean> ? "Y" : "N";
// 进行分发
type T0 = Naked<number | boolean>; // "N" | "Y"
// 没有进行分发
type T1 = WrappedTuple<number | boolean>; // "N"
type T2 = WrappedArray<number | boolean>; // "N"
type T3 = WrappedPromise<true | false>; // "Y"
// 🔥🔥 重要 ,
// 如果没有 & {},这一步的话,下面的 s1 不会进行分发,是 boolean
type NoDistrubate<T> = T & {}
type UnionAsset<T,U> = NoDistrubate<T> extends U ? true :false
type s1 = UnionAsset<false | number, boolean> // false
type Letter = "a" | "b" | "c";
// 原封不动的 Letter ,作为一个整体
// type Remove= Letter extends "c" ? never : Letter // 没有分发
// 没有被包裹, 有分发
type Remove<T> = T extends "c" ? never : T
type a = Remove<Letter> // 'a' | 'b'
断言 - assets 关键字
一个控制流,类似于 if else,只是更加优雅 🍷
assets(断言) 表示我知道这个值的类型,是联合类型的收紧
初始值可能是一个类型,后面你又修改成另一个类型,你很确定是你断言的这个类型
function assert(value: unknown, message?: string): asserts value {
if (!value) {
throw new Error(message);
}
}
function assertNonNull<T>(obj: T): asserts obj is NonNullable<T>{
if (!obj) {
throw new Error('不能是 null 或者 是 undefined');
}
}
function f1(n: number | string): number {
assert(typeof n === 'string');
return n.length; // 执行到这里 n 一定是 string 类型。
}
function f2(n: null | string) {
assertNonNull(n);
return n.length; // n -> string
}
f2("2")
在 vue 中,使用 ref 来引用一个dom 结构,往往初始值 是 HTMLElement | null
const domRef = ref<HTMLElement | null>(null)
// 你肯定知道 这个 domRef 是引用了一个dom,但是 ts 不知道
function assetsElement(ele: HTMLElement | null): asserts ele {
if (!ele) {
throw new Error(`${ele} is not exited`);
}
}
assetsElement(domRef.value)
// 现在 domRef.value 就是 一个 HTMLElement 类型了
有大用 - is 关键字
他和上文的
assets关键字一样,也是收紧语法
判断一个 unknown 类型 使用 if else 判断有什么问题呢?
function isString(s: unknown): boolean {
return typeof s === 'string'
}
function toUpperCase(x: unknown) {
if(isString(x)) {
x.toUpperCase() // Error, Object is of type 'unknown'
}
}
⭐ 虽然在上一行明明已经通过
isString()函数确认参数 x 为 string 类型,但是由于函数嵌套 TypeScript 不能进行正确的类型判断
那么这种情况就是 is 出手的时候了
const isString = (s: unknown): s is string => typeof s === 'string'
function toUpperCase(x: unknown) {
if(isString(x)) {
x.toUpperCase()
}
}
这时, x 已经被收紧为
string类型了
常用的判断类型函数
const isNumber = (val: unknown): val is number => typeof val === 'number'
const isString = (val: unknown): val is string => typeof val === 'string'
const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
const isFunction = (val: unknown): val is Function => typeof val === 'function'
const isObject = (val: unknown): val is Record<any, any> => val !== null && typeof val === 'object'
function isPromise<T = any>(val: unknown): val is Promise<T> {
return isObject(val) && isFunction(val.then) && isFunction(val.catch)
}
const objectToString = Object.prototype.toString
const toTypeString = (value: unknown): string => objectToString.call(value)
const isPlainObject = (val: unknown): val is object => toTypeString(val) === '[object Object]'
新语法有奇效 satisfies
出现上图的原因 大家应该都可能遇到过,按理说,ts 应该可以判断出来 我妻子的心情是 happy
为啥还是 联合类型呢? 因为 ts 只能推倒出xcc是满足类型 wife 的,其他的推倒就无能为力了,这个时候,是satisfies 上场的时候了
interface Wife {
mood: "happy" | "sad";
}
const xcc = {
mood: "happy",
} satisfies Wife;
看效果
再来一个🌰
下面的写法你一定见过
interface IUser {
id:number
image: string | {
width: string,
img:HTMLImageElement
}
}
const badImage:IUser = {
id:1,
image:"aa"
}
// 只能获取字符串和 对象的公有方法
badImage.image
你一定以为badImage.image 一定是个字符串了, 年轻人,还是太年轻了
是一个联合类型,使用 satisfies 试试?
const goodImage = {
id:1,
image:"aa"
} satisfies IUser
你以为satisfies 只有这些吗? nonono
type Person = {
name:string,
age:number
[x:string]:any
}
let tsk:Person = {
name:"tsk",
age:26,
address:"杭州"
}
那么 tsk 是否可以推到出 address 呢?
答案是:不能
还得是你 satisfies
筛选字符串
有时候一个 类型可以填写任何的字符串类型,但是你还有些常用的 字符串,但是还需要提示
type C = "sm" | "md" | Omit<string,"sm"|"md">; // 不能 string,否则 sm 和 md 不生效
let g:C = "aaa"
let f:C = "md"
部分可选属性
怎么做到只让
hobby变成可选的?
interface User {
name: string;
age: number;
hobby: string;
}
思路是 忽略这个
hobby联合上hobby单独可选
// 拿出其他值 & 让这两个值 变成可选
type C<T,S extends keyof T> = Omit<T,S> & Partial<Pick<T,S>>
type Computed<T> = {
[ k in keyof T ] : T[k]
};
type d = Computed<C<User, 'hobby'>>
🔥🔥 获取对象的key值
fn接收一个对象,返回一个函数i
你的目的是 在函数i中传入 接收对象 的key
🧐 怎么实现这个Path类型?
function fn<S>(schema: S): (path: Path<S>) => void {
return path => {};
}
const i = fn({
home: {
toolbar: {
title: "title",
welcome: "welcome",
},
},
login: {
userName: "用户名",
age: 20,
},
});
// 怎么实现 ?
i("home.toolbar.welcome");
📓 思路:
如果按照 js 的想法来说
- 首先是 遍历这个传入的对象,获取对象的 key,value
- 判断这个 value 的类型
- 如果是字符串 那么就要收集自己和父亲的key 值
- 如果是 对象,那么就要 继续 遍历
type Path<T, F extends string = "", K = keyof T> =
K extends keyof T // 泛型无法保证类型
// 判断是否是 对象
? T[K] extends object
// 如果是对象 就继续收集,F 如果没有,那么说明是第一层的对象,这个时候不需要用 .
? Path<T[K], `${F}${F extends "" ? "" : "."}${K & string}`, keyof T[K]>
// 不是对象,就带上父亲 和 自己的 K 值
: `${F}.${K & string}`
: any;
🚀 url 的 search 转对象
url 中 的 search
var url = new URL('https://developer.mozilla.org/en-US/docs/Web/API/URL/search?q=123');
var queryString = url.search; // Returns:"?q=123"
定义一个QueryParams类型,可以获取 search类型,🧐怎么办?
const str = "?age=12&name=zs";
// || 怎么转化
const obj:QueryParams = {
age:"12",
name:'zs',
};
📓 思路:
如果按照 js 的想法
- 把这个
str的字符串 以?的分割符 切开,只要 右边的 str- 切割后的右边str 还要以
&的分隔符 切开,形成age = 12和name = zs两部分- 遍历切割后的两部分,在遍历的过程中,用
=分隔符进行切割,左边为key,右侧为value,使用对象进行不断的收集- 返回这个收集后的对象即可
道理是这个道理,但是实现起来还是有点困难😥
- 切割字符
- S 为 传入的 字符
-flag 为 要切割的字符
-R 为结果集
如果传入的 S 满足
${字符串(infer L)}flag${字符串(infer rest)}这种结构,就要拆分 rest
同时 用 R 把 L,rest 收集起来
直到 rest 不满足了上面的数据结构,抛出 R有人就要问了,收集 rest 可以理解,收集 L 是为了什么呢?
是为了最后一步 用=分割,左边 key 我要,右边的 value 我也要
type SplitStr<
S extends string,
flag extends string = '?',
R extends string[] = []> = S extends `${infer L}${flag}${infer rest}` ?
SplitStr<rest,flag,[...R,L,rest]> : R;
开始进行第一步和第二步
type SecondQuery = SplitStr<typeof str,"?">[1];
type ThirdQuery = SplitStr<SecondQuery,"&"> // ["age=12", "name=zs"]
第三步 和 第四步就顺理成章了
type QueryParams = {
[K in ThirdQuery[number]]:{
[T in SplitStr<K,"=">[0]] : SplitStr<K,"=">[1];
};
}[ThirdQuery[number]]
有可能小伙伴不理解ThirdQuery[number]这种写法,这是索引访问类型,可以戳这里查看更多关于👉索引访问类型
// ThirdQuery = ["name=zs","age = 12"]
type S = ThirdQuery[number] // "age=12" | "name=zs"
vue 项目
🐕:以下 均为<script setup>
为 props 标注类型
1.运行时声明
<script setup lang="ts">
const props = defineProps({
name: { type: String, required: true },
age: {
type:Number,
default:26,
validator: value => {
return value >= 0 // 除了验证是否符合type的类型,此处再判断该值结果是否符合验证
}
}
})
props.name // string
props.age // number
</script>
2. 基于类型的声明
<script setup lang="ts">
const props = defineProps<{
name: string
age?: number
}>()
</script>
这种的不足之处在于失去了定义 props 默认值的能力,我们可以使用 withDefaults 编译器宏:
<script setup lang="ts">
interface Props {
name: string
age?: number
}
const props = withDefaults(defineProps<Props>(), {
name: 'tsk',
age: 26
})
</script>
3.🚶❌ 数组定义(不建议使用)
const props = defineProps(['foo', 'bar'])
这个在源码中这样定义的
export declare function defineProps<PropNames extends string = string>(props: PropNames[]): Readonly<{
[key in PropNames]?: any;
}>;
这个不建议使用的原因是因为,没有很好的利用 ts 带来的好处
为 emits 标注类型
1.运行时声明
<script setup lang="ts">
// 运行时
const emit = defineEmits(['change', 'update'])
</script>
2. 基于类型
// 基于类型
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
3. 外部定义
a.ts
export const checkEmit = {
change: (val: Boolean) => true,
};
xxx.vue
import { checkEmit } from "./a";
const emits = defineEmits(checkEmit)
这个在一些 UI 库里最为常见, 页面逻辑清晰
但是如果 checkEmit 这样返回就会有一个警告⚠
export const checkEmit = {
change: (val: Boolean) => false,
};
原因在这里,其实这个返回值并不是很重要,true / false 关系不大
结语
其实在实际项目中,遇到过很多很多有趣的 ts 类型,ts 虽然让我们的代码变得更加健壮,但是也会失去 js 应有的灵活性,合理使用 ts,不要为了 追求那一点点的代码提示,错误检查❌ 而花费大量的时间,做项目嘛,还是要考虑性价比的,希望以后可以能够更好的掌握 ts,深入的讨论 ts 中那些有趣的语法