TS思考 - 函数

已经是 TS 思考篇的第三篇了,今天来说说函数

自己在写 TS 的时候,有很多困惑都来自函数,也不是说都是在定义函数类型的时候遇到的困难,有很多地方都会涉及到函数

所以今天,就来看一看之前一直弄不懂的一些概念和新学到的在写 TS 时,函数相关的注意事项。

What is call signature?

一直不太明白call signature的作用是什么。

先来看看什么是call signature

interface TestType {
  (arg1: number): number;
  test1: string;
  test2: boolean;
}

例子中 (arg1: number): number;就是类型TestType的call signature;

这有什么用呢?单独的把它拎出来看,其实

interface TestType {
  (arg1: number): number;
}

type TestType2 = (arg1: number) => number;

在上面这个例子中,TestTypeTestType2其实是等价的,都是限定了一个函数的类型,接受一个参数,类型为number,返回值的类型为number

那么就有小伙伴问了,那call signature和函数类型的定义有什么区别呢?

如果单纯地拿类似于TestType这样的类型(也就是interface中仅有一个call signature的定义)来和函数类型比较。

我只能说call signature和函数类型完全一样,没啥区别。

但是!!!!

我们不能忘了interface本身是定义一个对象的类型,它能做的事情,比单纯的函数类型的定义要多,所以这就是为什么call signature存在的意义了。

为什么带泛型的箭头函数必须加限制?

其实这个问题应该被再细化一些:

函数 props 的类型定义带有泛型的函数组件,在使用箭头函数方式书写时,泛型必须通过extends来进行一次类型收紧(收紧)。


// 错误
const TestComponent = <T>(props:T) => {
  // TODO
}

就像上述的例子和图片里一样,如果使用箭头函数定义一个props是泛型类型的component,就会报错。

差了很多资料,找到了几种解决方式:

const TestComponent1 = <T,>(props:T) => {
  // TODO
}

const TestComponent2 = <T extends unknown>(props:T) => {
  // TODO
}

const TestComponent3 = <T extends {}>(props:T) => {
  // TODO
}

到底是为什么必须要使用上述这种写法,才可以不报错呢?为什么必须要extends或者加一些东西才行呢?

最终还是决定来看看React.FC的类型定义

// FC
type FC<P = {}> = FunctionComponent<P>;

// FunctionComponent
interface FunctionComponent<P = {}> {
  (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
  propTypes?: WeakValidationMap<P>;
  contextTypes?: ValidationMap<any>;
  defaultProps?: Partial<P>;
  displayName?: string;
}

// PropsWithChildren
type PropsWithChildren<P> = P & { children?: ReactNode };

可以看到其实FC是为了我们平时开发方便而给定的类型简称。真正的类型是FunctionComponent。我们来分析一下FunctionComponent的类型中的这句:

(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;

这是我们刚才刚刚说过的概念call signature,也就意味着被声明为这个类型的值是可以像函数一样被调用的。

根据查阅一些资料,了解到一些平时不知道的写法

比如现在假装忽略掉我们要写的泛型写法

interface Ab {
  a: number;
  b: number;
}

return (
<DataList<Ab>
  collapsed={false}
  listOfData={[{a: 1, b: 2}, {a: 3, c: 4}]}
/>

上述这种写法,其实就如同我们想要实现的写法MyProps<Ab>,结合一下刚才我们一起看的FunctionComponent的定义,

其实不是箭头函数的泛型要求有限制,而是FunctionComponent不允许传递一个泛型类型作为自己的类型参数参数 ,仅此而已。

Generic Function 的好习惯

坐实类型参数

function firstElement1<Type>(arr: Type[]) {
  return arr[0];
}
 
function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0];
}
 
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);

这个例子最开始猛地一看感觉差不多,后者比前者多了一个类型限制,但是恰恰是因为这个类型限制,导致b的类型TS只能推断出来是anya会被TS推断出来是number

是因为TS会根据约束的类型来推断返回值的类型,而不会等待真的调用这个函数的时候,去推导类型。

所以在写函数时,能单纯的使用类型本身,就不要去限制它。

用更少的类型参数

还是老样子,先上例子

function test1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
  return arr.filter(func);
}
 
function test2<Type, Func extends (arg: Type) => boolean>(
  arr: Type[],
  func: Func
): Type[] {
  return arr.filter(func);
}

可能(我是说可能),有同学认为第二种写法会高级一些(因为用到泛型总比不用高级)。

其实这个同学,就是最开始学习TS的我。总觉得泛型比普通类型高级,但其实,比如上面这个例子,

Func在函数中甚至就使用了一次,且Func甚至没有关联多种类型可能性(就是Funcextends后已经定死了)。

这样是完全没有必要的。

类型参数至少能接收2种不同的类型

function tired<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}
 
greet("but world sucks.");

这里其实有点像第二种情况,泛型本身有点像函数重载(参数个数位置相同类型不同的情况下)。我们至少允许这个类型参数能接受2种不同的类型,才能达到泛型最基本的功能。

例子中Str这个类型参数已经被限制为仅是string类型,完全失去了泛型基本的意义。

参考