Typescript入门 | 青训营笔记

81 阅读10分钟

Typescript入门

这是我参与「第四届青训营 」笔记创作活动的第1天

这边笔记是根据课上的讲解所做的记录,我对于TS的了解还不深入,可能有遗漏或不准确的地方,还请谅解

为什么我们需要Typescript?

动态类型/弱类型语言 静态类型/弱类型语言

动态类型 VS 静态类型:

  • 动态语言是在运行时确定数据类型的语言。变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型;静态语言是在编译时变量的数据类型即可确定的语言,多数静态类型语言要求在使用变量之前必须声明数据类型。
  • 动态类型语言的变量类型在运行期是可变的,这意味着对象的多态性是与生俱来的。一个对象能否执行某个操作,只取决于有没有对应的方法,而不取决于它是否是某种类型的对象;静态类型语言编译时会进行类型匹配检查,所以不能给变量赋予不同类型的值。为了解决这一问题,静态类型的面向对象语言通常通过向上转型的技术来取得多态的效果。
  • 动态语言的优势: 编写的代码数量更少,看起来更加简洁,可以把精力更多地放在业务逻辑上。虽然不区分类型在某些情况下会让程序变得难以理解,但整体而言,代码量越少,越专注于逻辑表达,对阅读程序越有帮助。静态语言的优势: 由于类型的强制声明,使得IDE有很强的代码感知能力,故在实现复杂的业务逻辑、开发大型商业系统、以及那些生命周期很长的应用中,依托IDE对系统的开发很有保障;由于静态语言相对比较封闭,使得第三方开发包对代码的侵害性可以降到最低。

强类型语言 VS 弱类型语言:

  • 强类型语言要求变量的使用要严格符合定义,所有变量都必须先定义后使用。一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。强类型语言有C#、Java等。
  • JS和TS都是弱类型语言,即数据类型可以被忽略的语言。它与强类型定义语言相反, 一个变量可以赋不同数据类型的值。你想把这个变量当做什么类型来用,就当做什么类型来用,语言的解析器会自动(隐式)转换。

TS与JS的区别:

  1. 静态类型
  • 可读性增强:基于语法解析TSDoc,IDE增强
  • 可维护性增强:在编译阶段暴露大部分错误,在多人维护的大型项目中,获得更好的稳定性和开发效率。
  1. TS是JS的超集
  • 包含兼容所有JS特性,支持共存
  • 支持渐进式引入与升级

TS基本语法

简单来讲,TS是加上了类型声明的JS,那么就让我们来看看TS是如何给各个类型做声明,从而将JS向TS转化的:

基础数据类型

JS

// 字符串
const q = "string";
// 数字
const w = 1;
// 布尔值
const e = true;
// null
const r = null;
// undefined
const t = undefined;

TS

// 字符串
const q: string = "string";
// 数字
const w: number = 1;
// 布尔值
const e: boolean = true;
// null
const r: null = null;
// undefined
const t: undefined = undefined;

对象类型

TS

const bytedancer: IBytedancer = {
    jobId: 9303245,
    name: 'Lin',
    sex: 'man',
    age: 28,
    hobby: 'swimming',
}

interface IBytedancer {
    // 只读属性:约束属性不可在对象初始化外赋值
    readonly jobId: number;
    name: string;
    sex: 'man' | 'woman' | 'other';
    age: number;
    // 可选属性:定义该属性可以不存在
    hobby?: string;
    // 任意属性:约束所有对象属性必须是该属性的子类型
    [key: string]: any;
}

// 报错:无法分配到 "jobId" ,因为它是只读属性。
bytedancer.jobId = 12345;
// 成功:任意属性标注下可以添加任意属性
bytedancer.palteform = 'data';
// 报错:缺少属性"name",hobby可缺省
const bytedancer2: IBytedancer = {
    jobId: 8765,
    sex: 'woman',
    age: 18,
}

TS接口(interface): 接口就相当于是一个规范,凡是使用这个接口,就必须遵循这个接口的规范,这样便于类型的统一;接口并不会影响、操作内部的值,它只是检查值是否符合规范。

函数类型

JS

function add(x, y) {
    return x + y;
}
//箭头函数
const mult = (x, y) => x * y;

TS

function add(x: number, y: number): number {
    return x + y;
}
//箭头函数
const mult: (x: number, y: number) => number = (x, y) => x * y;

//上面的对箭头函数的类型声明是不是有些复杂?没关系,还有更简短的使用Interface的写法
interface Imult {
    (x: number, y: number): number;
}
const mult: Imult = (x, y) => x * y;

函数重载

//TypeScript函数重载是指为同一个函数提供多个函数类型,它的意义在于让你清晰的知道
//传入不同的参数得到不同的结果。如果不是这种情况, 那就不需要使用函数重载。
function getDate(type: 'string', timestamp?: string): string;
function getDate(type: 'date', timestamp?: string): Date;
function getDate(type: 'string' | 'date', timestamp?: string): Date | string {
    const date = new Date(timestamp);
    return type === 'string' ? date.toLocaleString() : date;
}

const x = getDate('date'); // x: Date
const y = getDate('string', '2018-01-10'); // y: string

//利用interface进行重载的例子
interface IGetDate {
    (type: 'string', timestamp?: string): string;
    (type: 'date', timestamp?: string): Date;
    (type: 'string' | 'date', timestamp?: string): Date | string;
}
//这里会有报错
// 不能将类型“(type: any, timestamp: any) => string | Date”分配给类型“IGetDate”。
//   不能将类型“string | Date”分配给类型“string”。
//     不能将类型“Date”分配给类型“string”。ts(2322)
const getDate2: IGetDate = (type, timestamp) => {
    const date = new Date(timestamp);
    return type === 'string' ? date.toLocaleDateString() : date;
}

interface IGetDate {
    (type: 'string', timestamp?: string): any;
    (type: 'date', timestamp?: string): any;
    (type: 'string' | 'date', timestamp?: string): Date | string;
}
// ↑ 将前两个返回类型改为any就不会报错了 ↑
//老师的解释是要让IGetDate的范围表达大于匿名函数的范围表达,我理解得还不是很透彻,需要后面查询相关资料
const getDate2: IGetDate = (type, timestamp) => {
    const date = new Date(timestamp);
    return type === 'string' ? date.toLocaleDateString() : date;
}

数组类型

// 类型 + 方括号 表示
type IArr1 = number[];
    // 泛型表示
// Record 后面的泛型就是对象(Object)的 Key 和 Value 的类型
type IArr2 = Array<string | number | Record<string, number>>;
// 元祖表示
type IArr3 = [number, number, string, string];
// 接口表示
interface IArr4 {
    [key: number]: any;
}

const arr1: IArr1 = [1, 2, 3, 4, 5, 6];
const arr2: IArr2 = [1, 2, '3', '4', { a: 1 }];
const arr3: IArr3 = [1, 2, '3', '4'];
// 对arr3再加元素的话会报错 ↓
// 不能将类型“[number, number, string, string, string]”分配给类型“IArr3”。
//   源具有 5 个元素,但目标仅允许 4 个。ts(2322)
const arr3: IArr3 = [1, 2, '3', '4', '1'];
const arr4: IArr4 = ['string', () => null, {}, []];

对于interface和type声明的区别,可以参考这篇文章

Typescript泛型

// 泛型

//假设我们要将下面这个JS函数用TS定义类型
function getRepeatArr(target) {
    return new Array(100).fill(target);
}
//因为我们不知道传入的target是什么类型的,所以用了传入参数和返回值都用了any
//但这样写,不管传入的target是什么类型的值,返回的类型都是any
type IGetRepeatArr = (target: any) => any[];
//正确的写法是使用泛型:不预先指定具体的类型,而在使用的时候再指定类型的一种特性
type IGetRepeatArr = <T>(target: T) => T[];

// 泛型接口 & 多泛型
interface IX<T, U> {
    key: T;
    val: U;
}

// 泛型类
class IMan<T> {
    instance: T;
}

// 泛型别名
type ITtpeArr<T> = Array<T>;

// 泛型约束:限制泛型必须符合字符串
type IGetRpeatStringArr = <T extends string>(target: T) => T[]; // 通过extends关键字将类型限制为字符串
const getStrArr: IGetRpeatStringArr = target => new Array(100).fill(target);
// 报错:类型“number”的参数不能赋给类型“string”的参数。
getStrArr(123);

// 泛型参数默认类型
type IGetRpeatArr<T = number> = (target: T) => T[]; 
const getRepeatArr: IGetRpeatArr = target => new Array(100).fill(target);
// 报错:类型“string”的参数不能赋给类型“number”的参数。
getRepeatArr('123');

类型别名 & 类型断言

// 通过type关键字定义了IObjArr的别名类型
type IObjArr = Array<{
    key: string;
    [objKey: string]: any;
}>
function keyBy<T extends IObjArr>(objArr: Array<T>) {
    // 未指定类型时,result类型为 {}
    const result = objArr.reduce((res, val, key) => {
        res[key] = val;
        return res;
    }, {});
    // 通过 as 关键字,断言result类型为正确类型
    return result as Record<string, T>;
}

字符串/数字 字面量

// 允许指定字符串/数字必须的固定值

// IDomTag必须为html、body、div、span中的其一
type IDomTag = 'html' | 'body' | 'div' | 'span';
// IOddNumber必须为1、3、5、7、9中的其一
type IOddNumber = 1 | 3 | 5 | 7 | 9;

Typescript补充类型

// 空类型,表示无赋值
type IEmptyFunction = () => void;

// 任意类型,是所有类型的子类型
type IAnyType = any;

// 枚举类型:支持枚举值到枚举名的正反向映射
enum EnumExample {
    add = '+',
    mult = '*',
}
EnumExample['add'] === '+';
EnumExample['+'] === 'add';

enum ECorlor { Mon, Tue, Wed, Thu, Fri, Sat, Sun};
ECorlor['Mon'] === 0;
ECorlor[0] === 'Mon';

高级类型

联合/交叉类型

  • 联合类型:IA | IB; 联合类型表示一个值可以是几种类型之一
  • 交叉类型:IA & IB; 多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性
//假设要为下面这个JS书籍列表数组编写类型
const bookList = [{
    author: 'xiaoming',
    type: 'history',
    range: '2001-2021',
}, {
    author: 'xiaoli',
    type: 'story',
    theme: 'love',
}]

//类型声明繁琐,存在较多重复
interface IHistoryBook {
    author: string;
    type: string;
    range: string;
}
interface IStoryBook {
    author: string;
    type: string;
    theme: string;
}
type IBookList = Array<IHistoryBook | IStoryBook>;

//利用联合/交叉类型改进
type IBookList = Array<{
    author: string;
} & ({
    type: 'history';
    range: string;
} | {
    type: 'story';
    theme: string;
})>

类型保护与类型守卫

interface IA { a: 1, a1: 2};

interface IB { b: 1, b1: 2};

function log(arg: IA | IB) {
    // 报错:类型“IA | IB”上不存在属性“a”。类型“IB”上不存在属性“a”。
    // 结论: 访问联合类型时,处于程序安全,仅能访问联合类型中的交集部分。
    if (arg.a) {
        console.log(arg.a);
    } else {
        console.log(arg.b);
    }
}

// 利用类型守卫改写
// 类型守卫:定义一个函数,它的返回值是一个类型谓词,生效范围为子作用域
function getIsIA(arg: IA | IB): arg is IA {
    return !!(arg as IA).a;
}

function log2(arg: IA | IB) {
    if (getIsIA(arg)) {
        console.log(arg.a1);
    } else {
        console.log(arg.b1)
    }
}
//实现函数reverse
//其可将数组或字符串进行反转
function reverse(target: string | Array<any>) {
    //TS内置typeof类型保护
    if (typeof target === 'string') {
        return target.split('').reverse().join('');
    }
    //TS内置instance类型保护
    if (target instanceof Object) {
        return target.reverse();
    }
}
// 实现函数logBook类型
// 函数接收书本类型,并logger出相关特征
function logBook(book: IBookItem) {
    // 联合类型 + 类型保护 = 自动类型推断
    if (book.type === 'history') {
        console.log(book.range);
    } else {
        console.log(book.theme);
    }
}
interface ISourceObj {
    x?: string;
    y?: string;
}
interface ITargetObj {
    x: string;
    y: string;
}
type IMerge = (sourceObj: ISourceObj, targetObj: ITargetObj)
=> ITargetObj;
// 类型实现繁琐:若obj类型较为复杂,则声明source和target便需要大量重复2遍
// 容易出错:若target增加/减少key,则需要source联动去除
interface IMerge {
    <T extends Record<string, any>>(sourceObj: Partial<T>, targetObj: T): T;
}

type IPartial<T extends Record<string, any>> = {
    [P in keyof T]?: T[P];
}

// 索引类型:关键字【keyof】,其相当于取值对象中的所有key组成的字符串字面量,如
type IKeys = keyof { a: string; b: number }; // => type IKeys = "a" | "b"

// 关键字【in】,其相当于取值字符串字面量中的一种可能,配合泛型P,即表示每个key
// 关键字【?】,通过设定对象可选选项,即可自动推导出子集类型

函数返回值类型

// 实现函数delayCall的类型声明
// delayCall接受一个函数作为入参,其实现延迟1s运行函数
// 其返回promise,结果为入参函数的返回结果
function delayCall(func) {
    return new Promise(resolve => {
        setTimeout(() => {
            const result = func();
            resolve(result);
        }, 1000);
    });
}
type IDelayCall = <T extends () => any>(func: T) => ReturnType<T>;

type IReturnType<T extends (...args: any) => any> = T extends (...args: any)
=> infer R ? R : any

// 关键字【extends】跟随泛型出现时,表示类型推断,其表达可类比三元表达式
// 如T===判断类型?类型A:类型B
// 关键字【infer】出现在类型推荐中,表示定义类型变量,可以用于指代类型
// 如该场景下,将函数的返回值类型作为变量,使用新泛型R表示,使用在类型推荐命中的结果中