TypeScript类型体操——First of Array

230 阅读2分钟

前言

简单题:First of Array
TS Playground:www.typescriptlang.org/play?#code/…

正文

题目

  14 - First of Array
  -------
  by Anthony Fu (@antfu) #easy #array

  ### Question

  Implement a generic `First<T>` that takes an Array `T` and returns its first element's type.

  For example:

  
  type arr1 = ['a', 'b', 'c']
  type arr2 = [3, 2, 1]

  type head1 = First<arr1> // expected to be 'a'
  type head2 = First<arr2> // expected to be 3
  

  > View on GitHub: https://tsch.js.org/14
*/

/* _____________ Your Code Here _____________ */

type First<T extends any[]> = any

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<First<[3, 2, 1]>, 3>>,
  Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>,
  Expect<Equal<First<[]>, never>>,
  Expect<Equal<First<[undefined]>, undefined>>,
]

type errors = [
  // @ts-expect-error
  First<'notArray'>,
  // @ts-expect-error
  First<{ 0: 'arrayLike' }>,
]

答案

// 方法一
type First<T extends any[]> = T extends [] ? never : T[0];
// 方法二
type First<T extends any[]> = T['length'] extends 0 ? never : T[0];
// 方法三
type First<T extends any[]> = T extends [infer A, ...infer rest] ? A : never;

总结
T extends U ? X : Y的形式为条件类型(Contional Types),即,如果类型T可以赋值给类型U,那么该表达式返回类型X,否则返回类型Y
infer关键字的作用简单说是:推导泛型参数。看下面的例子:

type numberPromise = Promise<number>;
type n = numberPromise extends Promise<infer P> ? P : nerver; // number

Promise输入了number获得一个新的类型,那么infer就可以通过已知的类型和获得它泛型反推出泛型参数。在TypeScript中infer就是用于从返回值得到参数,不过要注意它仅仅是推导,而非映射,遵循一套规则。还要注意,infer只能在extends的右边使用,infer P的P也只能在条件类型为true的一边使用。
下面给出详细的推导过程

type getIntersection<T> = T extends (a: infer P, b: infer P) => void ? P : never;
type Intersection = getIntersection<(a: string, b: number) => void>; // string & number

上面这个例子,可看出:

  1. infer必须在extends右侧使用,因为必须保证这个已知类型是由右侧的泛型推出来,不然推导它的参数还有什么意义呢?检查时会跳过使用了infer的地方。
  2. 遵循以下规则推导P,有四种情况:
    • P只在一个位置占位:直接推出类型
    • P都在协变位置占位:推出占位类型的联合/合集
    • P都在逆变位置占位:推出占位类型的交叉/交集(目前只有参数是逆变)
    • P既在顺变位置又在逆变位置:只有占位类型相同才能使extends为true,且推出这个占位类型
      上面的例子属于第三种情况,P都在逆变位置占位,最终就推出两个类型的交叉string & number,那么为何是这种关系,可以朴素地解释一下:因为(a: string, b: number) => void extends (a: infer P, b: infer P) => void,所以(a: string, b: number) => void子类型,所以P到string或者number是逆变,然而,这里反过来推P,所以string或number到P是协变,最终就推出string & number,当P只在一个位置占位时,它推出来的类型就是一一对应的,比如ParameterReturnType
      待续,这里的协变和逆变,后面会继续深入学习。。