typescript-extends

loading 2023年05月22日 63次浏览

这里是做ts类型体操时对于extends的用法不是特别熟悉,这里做一个总结记录:

1. 继承

传统的用法,可以继承接口,类等等:

  interface T1 {
    name: string
  }
  
  interface T2 {
    sex: number
  }
  
  // 多重继承,逗号隔开
  interface T3 extends T1,T2 {
    age: number
  }
  
  // 合法
  const t3: T3 = {
    name: 'xiaoming',
    sex: 1,
    age: 18
  }

2. 条件判断

2.1 基本判断和泛型判断

重点说一下这种用法,先看看普通的条件判断:

  interface Animal {
    eat(): void
  }
  
  interface Dog extends Animal {
    bite(): void
  }
  
  // A的类型为string
  type A = Dog extends Animal ? string : number
  
  const a: A = 'this is string'

可以得知A extends B的作用是:如果某个值在满足A的情况下一定能满足B,那么为真;否则如果满足A情况下不一定满足B,那么为假。
比如例子中,是狗就一定是动物,因此为真。

那么再稍微复杂一点,加入泛型:

  type A1 = 'x' extends 'x' ? string : number; // string
  type A2 = 'x' | 'y' extends 'x' ? string : number; // number
  
  type P<T> = T extends 'x' ? string : number;
  type A3 = P<'x' | 'y'> // string|number

泛型的特点是涉及到分配律的问题,注意对比A2和A3,输入的类型是一样的,但是结果有区别,是因为泛型中的联合类型会按照分配律分别进行计算:

P<'x' | 'y'> => P<'x'> | P<'y'>
// 'x'代入得到
'x' extends 'x' ? string : number => string
// 'y'代入得到
'y' extends 'x' ? string : number => number

然后将每一项代入得到的结果联合起来,得到string | number

2.2 阻止泛型分配

加入一个中括号包起来即可:

  type P<T> = [T] extends ['x'] ? string : number;
  type A1 = P<'x' | 'y'> // number

2.3 never

  // never是所有类型的子类型
  type A1 = never extends 'x' ? string : number; // string

  type P<T> = T extends 'x' ? string : number;
  type A2 = P<never> // never

为啥这里P会返回never而不是string呢?因为never相当于空的联合类型,也要按照分配律划分。 然而因为空的联合类型没有联合项可以分配,所以P的表达式其实根本就没有执行,所以A2的定义也就类似于永远没有返回的函数一样,是never类型的。

要解决这种问题,也是通过2.2中的方法处理即可,加个中括号包着:

  type P<T> = [T] extends ['x'] ? string : number;
  type A2 = P<never> // string

3. 约束

举个例子,我们想要编写一个函数,这个函数接受两个值,并返回较长的那个值。为了做到这一点,我们需要这两个值都有一个类型为数字的length属性。 我们可以通过编写extends子句来约束类型参数必须具有这个属性:

function longer<T extends { length: number }>(a: T, b: T): T {
    if (a.length >= b.length) {
        return a;
    } else {
        return b;
    }
}

在这个例子中,T extends { length: number }是一个类型约束,表示类型参数T必须是一个具有length属性的类型,而length属性的类型必须是number。这样,我们就可以在函数体中安全地访问a.length和b.length,并比较它们的大小。

再来看个例子加深印象:

function minimumLength<Type extends { length: number }>(
    obj: Type,
    minimum: number
): Type {
    if (obj.length >= minimum) {
        return obj;
    } else {
        return { length: minimum };
    }
}

这段代码乍一看上去没问题,实际上第8行会报错,为什么?

因为当obj.length < minimum时,函数会返回一个对象{ length: minimum },这个新对象并不一定符合Type类型。因为Type被约束为具有length属性的任何类型,所以它可能包含除length之外的其他属性。在这种情况下,返回的对象就不符合Type类型,这会导致类型错误。

比如我这样来执行这个函数:

const arr = minimumLength([1, 2, 3], 6);

Type类型此时为number[],和{ length: minimum }冲突,因为数组还具有很多其他方法,不只是一个length属性。

4. 实战

4.1 实现Exclude

这个高级类型的作用是根据去除第一个参数中包含第二个参数的部分:

type A = Exclude<'key1' | 'key2', 'key2'> // 'key1'

实现起来也很简单,就是利用了泛型分配律的思路:

type Exclude<T, U> = T extends U ? never : T

拆解开就是:

type A = `Exclude<'key1' | 'key2', 'key2'>`

// 等价于

type A = `Exclude<'key1', 'key2'>` | `Exclude<'key2', 'key2'>`

// =>

type A = ('key1' extends 'key2' ? never : 'key1') | ('key'2 extends 'key2' ? never : 'key2')

// =>

// never是所有类型的子类型
type A = 'key1' | never = 'key1'

4.2 实现Pick

这个高级类型的作用是从接口中将联合类型中的key对应的项挑选出来,组成新的接口:

interface A {
    name: string;
    age: number;
    sex: number;
}

type A1 = Pick<A, 'name'|'age'>

接下来是实现,同样是用到了extends的条件判断功能,将K限制在T的key中:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P]
}

如果K超出限制,就会导致报错:

interface A {
    name: string;
    age: number;
    sex: number;
}

// 报错:类型“"key" | "noSuchKey"”不满足约束“keyof A”
type A2 = Pick<A, 'name'|'noSuchKey'>

主要参考:https://juejin.cn/post/6998736350841143326#heading-6