深入函数:函数重载、this 和 as const

函数重载(overload)

重载允许一个函数在接受不同数量或类型的参数时,作出不同的处理

ts
function createDate(n: number): Date;
function createDate(year: number, month: number, date: number): Date;

// 中间不能写其他代码,函数类型声明后面只能接函数的实现

function createDate(a: number, b?: number, c?: number): Date {
  if (a !== undefined && b !== undefined && c !== undefined) {
    return new Date(a, b, c);
  } else if (a !== undefined && b === undefined && c === undefined) {
    return new Date(a);
  } else {
    throw new Error('传参错误');
  }
}

createDate(1677772800000);
createDate(2023, 2, 3);

关于函数重载:

  • 重载的思想来自于 Java 或 C#,因为它们都不支持联合类型
  • 重载是为了使同名函数可以接受不同类型的参数
  • 不是非得使用重载,即使不用函数重载也可以实现上述的功能
    ts
    function createDateFromNumber(n: number): Date {
      return new Date(n);
    }
    function createDateFromYMD(year: number, month: number, date: number): Date {
      return new Date(year, month, date);
    }
    
    createDateFromNumber(1677772800000);
    createDateFromYMD(2023, 2, 3);
    
  • 复杂度守恒,你不可能把复杂度凭空抹除掉,因此你只能选择把复杂度留给自己或者抛给用户。比如上面的例子中,使用函数重载就是把复杂度留给自己;而提供两个函数供用户选择就是把复杂度留给用户。
  • 箭头函数不支持函数重载,箭头函数是匿名函数,无法定义多个同名的箭头函数,应该使用传统的函数声明或函数表达式。

举例

使用 TypeScript 的函数重载和类型谓词来处理具有不同属性的组件 props

tsx
interface PropsA {
  isA: true;
  a1: boolean;
  a2: string;
  a3: number;
}

interface PropsB {
  isA: false;
  b1: boolean;
  b2: string;
  b3: number;
}

function isPropsA(x: PropsA | PropsB): x is PropsA {
  return x.isA;
}

export function FunctionComponent(props: PropsA): JSX.Element;
export function FunctionComponent(props: PropsB): JSX.Element;
export function FunctionComponent(props: PropsA | PropsB) {
  // ...
  if (isPropsA(props)) {
    // ...
  } else {
    // ...
  }
}

tsx
// isA 为 true 时,props 类型为 PropsA,a1、a2、a3 都必传
<FunctionComponent isA={true} a1={true} a2="hello" a3={123} />
// isA 为 false 时,props 类型为 PropsB,b1、b2、b3 都必传
<FunctionComponent isA={false} b1={true} b2="world" b3={456} />

这种模式允许你在同一个组件中安全地处理不同的属性集,同时保持类型的严格性和清晰性。

指定 this 的类型

ts
type Person = { name: string };
function f(this: Person, n: number) {
  console.log(this.name + n);
}

// 1. person.f(1)
const p: Person & { f: typeof f } = { name: 'ClariS', f };
p.f(1);
// 2. f.call(person, 1)
const p2: Person = { name: 'ClariS' };
f.call(p2, 1);
// 3. f.apply(person, [1])
const p3: Person = { name: 'ClariS' };
f.apply(p3, [1]);
// 4. f.bind(person)(1)
const p4: Person = { name: 'ClariS' };
const newF = f.bind(p4);
newF(1);
// 或
newF.bind(null, 1);

剩余参数

ts
function sum(name: string, ...array: number[]) {
  console.log(name);
  return array.reduce((result, n) => result + n, 0);
}
sum('one', 1);
sum('two', 1, 2);
sum('three', 1, 2, 3);

注意,剩余参数只能是最后一个参数

展开参数

ts
function sum(...array: number[]) {
  f.apply(null, array);
  // f(array[0], array[1], array[2], array[3], ...)
  f(...array);
}
function f(...array: number[]) {
  console.log(array);
}

常量断言(as const)

TS 会区别对待可修改和不可修改的值的类型推断

例如下面的 immutableString 会被推断成单值类型 'ClariS' 而 mutableString 则会被推断成通用的 string 类型

ts
const immutableString = 'ClariS'; // 'ClariS'
let mutableString = 'vivy'; // string

而在一般的对象中,由于对象的属性都具有可修改性,TS 都会对它们「从宽」类型推断,例如下面的 prop 的类型被推断为 string

ts
const obj = {
  prop: 'xxx' // string
};

根本原因在于 TS 会根据一个值在后续的逻辑中是否可能被修改而给出不同的类型推断结果:

  • 对于有可能被修改的值,TS 采用较为宽松的类型推断策略,即把上述 mutableString,obj.props 推断为较为宽泛的 string 类型,这使未来可能出现的赋值具有更大的灵活度
  • 对于不可能被重新赋值的值,TS 采用较为严格的类型推断策略,即把上述 immutableString 推断为单值类型 'ClariS',这样未来把immutableString 赋值给别的变量时,出现类型检查错误的可能性更小

关于这部分内容,详细可参考文章TypeScript夜点心:类型推断的策略

TS 常量断言

常量断言可以把一个值标记为一个不可篡改的常量,从而让 TS 以最严格的策略来进行类型推断

还是使用上述的例子,给他们分别加上 as const 后类型推断如下

ts
let mutableString = 'vivy' as const; // 'vivy'

const obj = {
  prop: 'xxx' as const // 'xxx'
};

再看看其他的例子

ts
const array1 = [1, 'hi']; // (string | number)[]
array1.push(2); // 不报错

const array2 = [1, 'hi'] as const; // readonly [1, "hi"]
array2.push(2); // 报错

从以上例子中可以看出,as const 会把类型推窄,并给类型加上 readonly

关于这部分内容,详细可参考文章TypeScript 夜点心:常量断言

const 与 as const 的区别

as const 中的 const 与我们声明常量时使用的 const 有什么区别呢?

其实两者无论是语法还是语义,都相当不同:

  • const 常量声明是 ES6 的语法,对 TS 而言,它只能反映该常量本身是不可被重新赋值的,它的子属性仍然可以被修改,故 TS 只会对它们做松散的类型推断
  • as const 是 TS 的语法,它告诉 TS 它所断言的值以及该值的所有层级的子属性都是不可篡改的,故对每一级子属性都会做最严格的类型推断

more about the function

完整示例

ts
type Config = {
  url: string;
  method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
  data?: unknown;
  headers?: unknown;
};

function ajax1({ url, method, ...rest }: Config = { method: 'GET', url: '' }) {
  console.log(url, method, rest);
}

function ajax2({ url, method, ...rest } = { method: 'GET', url: '' } as Config) {
  console.log(url, method, rest);
}

类型 Config 可以写在左边,也可以写在右边

void 返回值类型

ts
function f1(): void {
  return;
}

function f2(): void {
  return undefined;
}

function f3(): void {}

function f4(): void {
  return null; // 报错:Type 'null' is not assignable to type 'void'
}

type F5 = () => void;
const f5: F5 = () => {
  return;
};
泛型编程(上)
深入函数:声明、参数、返回值

评论区

评论加载中...