TS难点

415 阅读12分钟

1、is和as的区别?

在TypeScript中,isas都是用来进行类型断言或类型保护的关键字。

is用于类型守卫,用来判断一个变量是否是某个指定类型的实例。例如:

function isNumber(value: any): value is number {
    return typeof value === 'number';
}
​
if (isNumber(value)) {
    // value 现在被 TypeScript 当作 number 类型了
}

as用于类型断言,用来告诉编译器一个变量的具体类型。例如:

let someValue: any = 'this is a string';
​
let strLength: number = (someValue as string).length;

需要注意的是,as语法是在编译阶段使用,不会影响运行时代码。因此在使用as进行类型断言时,需要确保断言的类型是正确的,避免运行时出现类型错误。

2、使用is进行类型守卫和boolean的区别

在TypeScript中,使用is进行类型守卫和直接使用boolean进行条件判断的区别在于类型信息的处理方式。

  1. 使用is进行类型守卫:
function isNumber(value: any): value is number {
    return typeof value === 'number';
}
​
let value: any = 10;
​
if (isNumber(value)) {
    // value 现在被 TypeScript 当作 number 类型了
    console.log(value.toFixed(2));
}

在这个例子中,通过isNumber函数进行类型守卫后,TypeScript会根据返回结果将value当作number类型,从而允许我们使用该类型特有的方法。这样可以在编译期间就避免类型错误。

  1. 使用boolean进行条件判断:
let value: any = 10;
​
if (typeof value === 'number') {
    // value 在这里仍然被视为 any 类型
    console.log(value.toFixed(2)); // 此处会报错,因为 value 依然被当作 any 类型
}

在这个例子中,虽然通过条件判断为true,但是value依然被当作any类型。因此在编译期间并不能确保valuenumber类型,会导致可能的运行时类型错误。

因此,使用is进行类型守卫能够更加准确地告诉编译器变量的具体类型,从而提高代码的类型安全性。而直接使用boolean进行条件判断则无法实现类型守卫,仍然需要小心处理变量的类型转换。

3、断言的方式?

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。

类型断言有两种形式。 其一是“尖括号”语法:

let someValue: any = "this is a string";
​
let strLength: number = (<string>someValue).length;

另一个为as语法:

let someValue: any = "this is a string";
​
let strLength: number = (someValue as string).length;

两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有 as语法断言是被允许的。

4、interface和type的区别?

interface 接口,class可以实现一个接口,在ts中类型校验使用interface 主要是进行结构的校验。接口可以继承另外一个接口。

type是一个类型的别名

  • type 可以而 interface 不行

    • type 可以声明基本类型别名,联合类型,元组等类型
// 基本类型别名
type Name = string// 联合类型
interface Dog {
    wong();
}
interface Cat {
    miao();
}
​
type Pet = Dog | Cat
​
// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]

  • type 语句中还可以使用 typeof 获取实例的 类型进行赋值
// 当你想获取一个变量的类型时,使用 typeof
​
let div = document.createElement('div');
type B = typeof div
  • interface 可以而 type 不行
interface User {
  name: string
  age: number
}
​
interface User {
  sex: string
}
​
/*
User 接口为 {
  name: string
  age: number
  sex: string
}
*/

5、类型推断、类型注解和类型断言?

1、类型推断

ts会根据变量初始化的数据类型推断变量的类型,比如说下方代码中a会被推断为是一个string类型,如果再给a赋值一个number类型的数据,那么在编译时ts就会提示错误。

let a='hello';
a=11;  //error:Type 'number' is not assignable to type 'string'.

2、类型注解

在变量名后边使用:[类型名]的方式直接指定变量类型,这种方式更加直观

let a:string='hello';
​
//或者
let a:string;
a="hello";

3、类型断言

let arr=[1,2,3]
let result=arr.find(item=>item===2)
result*3

image-20240524155034004.png

在这里result的值可能为undefined,所以ts报了错。

在这里,如果你确保result的值就是一个number,可以使用断言消除报错。断言可以跳过类型检查

let arr=[1,2,3]
let result=arr.find(item=>item>2) as number
result*3

6、对象展开的限制?

首先,它仅包含对象 自身的可枚举属性。 大体上是说当你展开一个对象实例时,你会丢失其方法:

class C {
  p = 12;
  m() {
  }
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!

其次,TypeScript编译器不允许展开泛型函数上的类型参数。 这个特性会在TypeScript的未来版本中考虑实现。

7、TypeScript的核心原则?

TypeScript的核心原则之一是对值所具有的结构进行类型检查。

8、接口的作用?

在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

9、readonly vs const

最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用readonly

10、javascript和typescript中的重载

  • 函数重载: 函数项名称相同 但输入输出类型或个数不同的子程序,它可以简单地称为一个单独功能可以执行多项任务的能力。
  • TypeScript 的函数重载: 为同一个函数提供多个函数类型定义来进行函数重载,目的是重载的pickCard函数在调用的时候会进行正确的类型检查。
  • js 因为是动态类型,相同的函数名后面的会覆盖前面的函数,所以js中其实不存在真正的重载。js本身不需要支持重载,直接对参数进行类型判断即可;但是ts为了保证类型安全,支持了函数签名的类型重载,即多个overload signatures和一个implementation signatures
// 重载签名
function add(x:string,y:string):string;
function add(x:number, y:number):number;
​
    //实现签名 对外不可见
function add(x:string|number, y: number|string): number | string{
  if(typeof x === 'string'){
    return x + ',' + y;
  }else {
    // ts暂时不支持对函数重载后续参数的narrowing操作,如这里对x做了type narrowing但是对y没有做narrowing,需要手动的y做type assert操作
    return x+(y as number)
  }
}
let x = add(1,2) // number
let y = add("hello","world") // string
console.log({x,y})
​

11、函数的类型注解中使用:和=>的区别?

命名函数返回值注解用:

匿名函数返回值注解用=>

function identity<T>(arg: T): T {
    return arg;
}
​
let myIdentity: <T>(arg: T) => T = identity;

12、TS的严格模式

在开发中,应该打开严格模式,启动strict、strictNullChecks等更加严格的类型检查,以免出现一些不必要的错误

image-20240524160510621.png 在代码中,可以使用tsc --init命令快速创建一个ts的配置文件

image-20240524160510621.png strictNullChecks等类型检查根据实际情况启用

13、ts和js中类的区别?

  • class:它定义了一件事物的抽象特点,这里面就包括它的属性和方法。
  • 对象object:是类的实例,通过new关键字创建。

在JavaScript中,生成实例对象的传统方法是通过构造函数。

function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype.toString = function() {
  return '(' + this.x + ', ' + this.y + ')';
}
var p = new Point(1, 2);
console.log(p.toString());
//(1,2)

在ES6中可以使用类class去进行构造。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return '(' + this.x + ',' + this.y + ')';
    }
}
var p = new Point(1, 2);
console.log(p.toString());
//(1,2)

可以看到,二者直观上的区别体现在“类”写法上的不同,在使用上没有区别。

基本上ES6class可以看作只是一个语法糖,它的绝大部分功能ES5都可以做到。

JavaScript中并没有如其它语言那般的publicprivate等修饰符,而这些内容却在TypeScript中实现了。

TypeScript中,每一个成员默认都是public的,另外两个修饰符分别是privateprotected

14.ts的类型兼容?

在ts中,类型兼容性是基于结构子类型的。结构类型是一种只使用其成员来描述类型的方式。

interface Named {
    name: string;
}
​
class Person {
    name: string;
}
​
let p: Named;
// OK, because of structural typing
p = new Person();

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
​
let status = Status.Ready;
status = Color.Green;  // Error

15.ts的声明合并?

interface、namespace等 可以合并。TypeScript并非允许所有的合并。 目前,类不能与其它类或变量合并,可以模仿类的合并。 想要了解如何模仿类的合并,请参考 TypeScript的混入

合并接口

interface Box {
    height: number;
    width: number;
}
​
interface Box {
    scale: number;
}
​
let box: Box = {height: 5, width: 6, scale: 10};

合并命名空间

Animals声明合并示例:

namespace Animals {
    export class Zebra { }
}
​
namespace Animals {
    export interface Legged { numberOfLegs: number; }
    export class Dog { }
}

等同于:

namespace Animals {
    export interface Legged { numberOfLegs: number; }
​
    export class Zebra { }
    export class Dog { }
}

16.ts的装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明方法属性或者参数上,

  • 语法:装饰器使用 @expression 这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入
  • 若要启用实验性的装饰器特性,必须tsconfig.json里启用experimentalDecorators编译器选项
  • 常见的装饰器有: 类装饰器属性装饰器方法装饰器参数装饰器
  • 装饰器的写法: 分为普通装饰器(无法传参)装饰器工厂(可以传参)

17、.ts和.d.ts的区别?

.ts 是标准的 TypeScript 文件。其内容将被编译为 JavaScript。

*.d.ts 是允许在 TypeScript 中使用现有 JavaScript 代码的类型定义文件。

例如,我们有一个简单的 JavaScript 函数,用于计算两个数字的总和:

// math.js
const sum = (a, b) => a + b
​
export { sum }

TypeScript 没有关于函数的任何信息,包括名称、参数类型。为了在 TypeScript 文件中使用该函数,我们在 d.ts 文件中提供其定义:

// math.d.ts
declare function sum(a: number, b: number): number

现在,我们可以在 TypeScript 中使用该函数,而不会出现任何编译错误。

d.ts 文件不包含任何实现,并且根本不编译为 JavaScript。

TypeScript 提供了一个名为 allowJs 的选项,允许在 TypeScript 中使用普通 JavaScript 代码。

// tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    ...
  }
}

当我们想要将现有的代码库从普通 JavaScript 迁移到 TypeScript 时,它非常有用。因此,我们可以将每个 JavaScript 文件逐个转换为 TypeScript,而不必完全转换整个代码。

参考:www.tslang.cn/docs/handbo…

18、ts中的declare

.d.ts 文件中的顶级声明必须以 "declare" 或 "export" 修饰符开头。

通过declare声明的类型或者变量或者模块,在include包含的文件范围内,都可以直接引用而不用去import或者import type相应的变量或者类型。

1.declare声明一个类型

declare type Asd {
    name: string;
}

在include包含的文件范围内可以直接使用Asd这个type

2.declare声明一个模块 最经典的声明模块应该是这样了

declare module '*.css';
declare module '*.less';
declare module '*.png';

在编辑ts文件的时候,如果你想导入一个.css/.less/.png格式的文件,如果没有经过declare的话是会提示语法错误的

19、ts中的混入Mixin和vue中的Mixin

Vue中Mixin的使用:全局使用和局部使用

定义Mixin

//house.js
var house = {   // 我们戈壁老王的房子(我们定义的mixin)
  data() {
    return { 
      Kitchen: '厨房',
      Tv: '电视机',
    }
  },
  filters: {
    cook: function (name) {
      if (!name) return ''
      return name + "再用厨房做饭……";
    },
    watch: function (name) {
      return name + '看电视……';
    }
  },
  created: function() {
    setTimeout(function(){ console.log("what?"); }, 3000);
    console.log("???");
  },
  methods: {
    runing: function () {
      console.log( `${this.laoWang }跑步`)       // 不会被执行!!
    }
  }
}

局部使用

improt house from './house.js'
export default {
  name: 'app',
  mixins: [house],
  data() {
    return { 
      smallMing: '小明',
      bigMing: '大明',
      laoWang: '老王',
    }
  },
  created: function() {
    console.log(this.Kitchen); // 厨房
    console.log(this.Tv);      // 电视机
    this.runing()              // 老王跑步呢,别打扰他。
  },
  methods: {
    runing() {   
      console.log( `${this.laoWang }跑步呢,别打扰他。`)
    }
  }
}
​

全局使用

 //在main.js 中引入
 import { house } from "./mixin/house";
 Vue.mixin(house)

TS中的混入

1.对象混入 可以使用es6的Object.assign 合并多个对象

此时 people 会被推断成一个交差类型 Name & Age & sex;

interface Name {
    name: string
}
interface Age {
    age: number
}
interface Sex {
    sex: number
}
​
let people1: Name = { name: "小满" }
let people2: Age = { age: 20 }
let people3: Sex = { sex: 1 }
​
const people = Object.assign(people1,people2,people3)

2.类的混入 首先声明两个mixins类 (严格模式要关闭不然编译不过)

class A {
    type: boolean = false;
    changeType() {
        this.type = !this.type
    }
}
class B {
    name: string = '张三';
    getName(): string {
        return this.name;
    }
}

下面创建一个类,结合了这两个mixins

首先应该注意到的是,没使用extends而是使用implements。 把类当成了接口

我们可以这么做来达到目的,为将要mixin进来的属性方法创建出占位属性。 这告诉编译器这些成员在运行时是可用的。 这样就能使用mixin带来的便利,虽说需要提前定义一些占位属性

class C implements A,B{
    type:boolean
    changeType:()=>void;
    name: string;
    getName:()=> string
}

最后,创建这个帮助函数,帮我们做混入操作。 它会遍历mixins上的所有属性,并复制到目标上去,把之前的占位属性替换成真正的实现代码

Object.getOwnPropertyNames()可以获取对象自身的属性,除去他继承来的属性, 对它所有的属性遍历,它是一个数组,遍历一下它所有的属性名

Mixins(C, [A, B])
function Mixins(curCls: any, itemCls: any[]) {
    itemCls.forEach(item => {
        Object.getOwnPropertyNames(item.prototype).forEach(name => {
            curCls.prototype[name] = item.prototype[name]
        })
    })
}

20、ts中extends和implements的关系

因为类可以作为接口使用,或者说因为类同时实现了接口,所以 TypeScript 中可以有如下关系:

  • 类继承类
  • 接口继承接口/类
  • 类实现接口/类