Typescript - 超越基础

2025-05-25

Typescript - 超越基础

简介

如果您发现自己有以下情况,那么这篇博文适合您

TypeScript 自动完成插件 meme

这不是入门教程。我不会介绍绝对基础知识,也不会介绍诸如、 、stringnumber类的类型注释。我假设您以前使用过 TypeScript。这篇博文首先简要解释以下概念:booleanArrayRecord

  • 类型推断
  • 交集和并集类型
  • 关键词:typeofkeyofas const
  • 类型缩小

然后深入探讨更高级的主题,例如:

  • 泛型
  • 定影Object.keys
  • 编写类型安全的省略函数

TypeScript 很棒,但我见过很多代码库不怎么使用 TypeScript,反而滥用它。我希望你能利用这篇博文中的知识来重构一些现有的 TypeScript 代码,并:

  • 在构建时捕获一些错误
  • 受益于更好的智能感知
  • 并使用 TypeScript 的类型推断来编写较少的类型

好的。我们开始吧!


类型推断

下面是声明数组时从 TypeScript 进行类型推断的示例:

const array = [1, '42', null]; // typeof array: (string | number | null)[]
const item = array[0]; // typeof item: string | number | null
array.push(true); // Argument of type 'true' is not assignable to parameter of type 'string | number | null'

// ---

// you can use a type annotation to also support "boolean" values
const array: (string | number | null | boolean)[] = [1, '42', null];
array.push(true); // ok
Enter fullscreen mode Exit fullscreen mode

另一个带有对象的例子:

const obj = { a: 'a', b: 'b' }; // typeof obj: { a: string; b: string; }
// obj.c = 'c'; // Property 'c' does not exist on type '{ a: string; b: string; }'

// ---

// you can use a type annotation to also support other string keys than "a" and "b"
const obj: { [Key: string]: string } = { a: 'a', b: 'b' };
obj.c = 'c'; // ok
Enter fullscreen mode Exit fullscreen mode

let之间的区别也很有趣const

let aLetString = 'test'; // type: string
const aConstString = 'test'; // type: "test"

let aLetNumber = 1; // type: number
const aConstNumber = 1; // type: 1

const takeString = (x: string) => x;
const result = takeString(aConstString); // typeof result: string
Enter fullscreen mode Exit fullscreen mode

"test"你有没有注意到,我们给函数传递了一个 类型的值:takeString?该函数接受一个 类型的参数string,但允许我们传递 类型的值"test"而不会出现任何错误。原因如下:

字符串字面量类型可以被视为字符串类型的子类型。这意味着字符串字面量类型可以赋值给普通字符串,但反之则不行。

例子总是让它更清楚:

const B = 'B'; // typeof B: "B"
type A = string;
const test: A = B; // ok

// ---

type A = 'A';
const test: A = 'B'; // Type '"B"' is not assignable to type '"A"'
Enter fullscreen mode Exit fullscreen mode

交集和并集类型

&这里是(交集)和(并集)运算符的示例|

type Intersection = { a: string } & { b: number };
const test1: Intersection = { a: 'a', b: 1 }; // ok
const test2: Intersection = { a: 'a' }; // Property 'b' is missing in type '{ a: string; }' but required in type '{ b: number; }'

// ---

type Union = { a: string } | { a: number };
const test1: Union = { a: 'a' }; // ok
const test2: Union = { a: 1 }; // ok
Enter fullscreen mode Exit fullscreen mode

对象类型的typeand运算符有所不同。您不能对接口使用and运算符,但可以对类型使用。我个人总是使用类型,因为它们没有限制。但是,您可以使用关键字,或者使用类型来创建两个现有接口的并集:interface&|extends

interface A { a: string }
interface B extends A { b: number }
const test1: B = { a: 'a', b: 1 }; // ok
const test2: B = { a: 'a' }; // Property 'b' is missing in type '{ a: string; }' but required in type 'B'

// ---

interface A { a: string }
interface B { a: number }
type Union = A | B;
const test1: Union = { a: 'a' }; // ok
const test2: Union = { a: 1 }; // ok
Enter fullscreen mode Exit fullscreen mode

关键字:typeofkeyofas const

也许您以前见过或使用过类型typeof和。似乎在野外并不常用,但我非常喜欢它。keyofas const

const obj = { a: 'a', b: 'b' };
type Obj = typeof obj; // { a: string; b: string; }

// ---

const obj = { a: 'a', b: 'b' };
type Key = keyof typeof obj; // "a" | "b"

// ---

const obj = { a: 'a', b: 'b' } as const;
type Obj = typeof obj; // { readonly a: "a"; readonly b: "b"; }
Enter fullscreen mode Exit fullscreen mode

如您所见,关键字as const还将对象的值设置为字符串文字类型("a""b"不是string)。让我们仔细看看as const关键字以及替换枚举的潜在用例。

// https://www.typescriptlang.org/play?target=99&jsx=0#code/AQ4UwOwVwW2BhA9lCAXATgT2AbwFCiHACCAKgDQFEgAiAopdSPABKOgC+QA
enum Country {
    AT,
    DE,
    CH,
}

// gets compiled to:
let Country;
(function (Country) {
    Country[(Country['AT'] = 0)] = 'AT';
    Country[(Country['DE'] = 1)] = 'DE';
    Country[(Country['CH'] = 2)] = 'CH';
})(Country || (Country = {}));
Enter fullscreen mode Exit fullscreen mode

如果你在运行时记录 的值Country.AT,你会发现它的值是数字0。我不喜欢以数字为值的枚举,因为现在你的数据库中已经有了这个数字,而代码中没有枚举定义,你就无法知道这个数字的含义。在我看来,具有字符串值的枚举更好,因为它们具有语义含义。还有另一种enum使用字符串值编写枚举的方法:

// https://www.typescriptlang.org/play?target=99&jsx=0&ssl=5&ssc=6&pln=1&pc=1#code/AQ4UwOwVwW2BhA9lCAXATgT2AbwFCiHACCAKsALzABEZ1ANAUSACICilN7DTz8AEp2oCehAL5A
enum Country {
    AT = 'AT',
    DE = 'DE',
    CH = 'CH',
}

// gets compiled to:
var Country;
(function (Country) {
    Country["AT"] = "AT";
    Country["DE"] = "DE";
    Country["CH"] = "CH";
})(Country || (Country = {}));
Enter fullscreen mode Exit fullscreen mode

那么我们如何使用as const来编写类似的东西enum

const Country = {
    AT: 'AT',
    DE: 'DE',
    CH: 'CH',
} as const;

const values = Object.values(Country);
type Country = typeof values[number];

// gets compiled to:
const Country = {
    AT: 'AT',
    DE: 'DE',
    CH: 'CH',
};
Enter fullscreen mode Exit fullscreen mode

我把选择权留给你。最终选择哪个并不重要,但我喜欢的是,你可以立即获得as const变体的智能感知,而不需要在每个使用枚举的地方都导入它,当然,如果你愿意的话,你仍然可以这样做。

enum Country {
    AT = 'AT',
    DE = 'DE',
    CH = 'CH',
}

// you always need to import the Country enum to use this function
const doSomethingWithEnum = (country: Country) => country;

doSomethingWithEnum(Country.AT); // ok
// doSomethingWithEnum('AT'); // Argument of type '"AT"' is not assignable to parameter of type 'Country'

// However doSomethingWithEnum('AT') would lead to working javascript code!


// ---

const Country = {
    AT: 'AT',
    DE: 'DE',
    CH: 'CH',
} as const;

const values = Object.values(Country);
type Country = typeof values[number];

// intellisense support and no need to import the country object to use this function
const doSomethingWithCountry = (country: Country) => country;

doSomethingWithCountry('AT'); // ok
doSomethingWithCountry(Country.AT); // ok
// doSomethingWithCountry('US') // Argument of type '"US"' is not assignable to parameter of type '"AT" | "DE" | "CH"'
Enter fullscreen mode Exit fullscreen mode

除了可以替代枚举之外,as const它还可以用于其他用途。我将在下一节中向您展示另一个用例。


类型缩小

类型缩小可用于在函数中接受不同类型,但随后可以安全地缩小类型并对不同类型执行不同的操作:

const format = (value: string | number) => {
    if (typeof value === 'string') {
        // value is of type string and all string functions are available within the if block
        return Number.parseFloat(value).toFixed(2);
    } else {
        // value is of type number and all number functions are available within the else block
        return value.toFixed(2);
    }
};
Enter fullscreen mode Exit fullscreen mode

Typescript 具有非常棒的类型推断,它让我们可以根据公共属性的类型来缩小类型:

const a = { value: 'a' };
const b = { value: 42 };
type AOrB = typeof a | typeof b;

const takeAOrB = (aOrB: AOrB) => {
    if (typeof aOrB.value === 'string') {
        const { value } = aOrB; // typeof value: string
    } else {
        const { value } = aOrB; // typeof value: number
    }
};
Enter fullscreen mode Exit fullscreen mode

但是,如果它们没有共同的属性,而是有两个不同的属性,我们也可以缩小范围:

const a = { a: 'a' };
const b = { b: 42 };
type AOrB = typeof a | typeof b;

const takeAOrB = (aOrB: AOrB) => {
    if ('a' in aOrB) {
        const { a } = aOrB; // typeof a: string
    } else {
        const { b } = aOrB; // typeof b: number
    }
};
Enter fullscreen mode Exit fullscreen mode

在某些时候,引入kindtype属性变得实用,然后可以使用它来区分不同类型(此kind属性也可以在 switch 案例中使用):

const a = { kind: 'a' as const, value: 'a' };
const b = { kind: 'b' as const, value: 42 };
type AOrB = typeof a | typeof b;

const takeAOrB = (aOrB: AOrB) => {
    if (aOrB.kind === 'a') {
        const { value } = aOrB; // typeof value: string
    } else {
        const { value } = aOrB; // typeof value: number
    }
};
Enter fullscreen mode Exit fullscreen mode

好吧,我觉得这个挺简单的。现在我们来深入研究一下泛型。我用 TypeScript 很久了,但自己从来没有写过泛型。泛型可能看起来很吓人,但相信我,一旦你学会了如何使用泛型,它就会为你打开一个全新的世界,解锁一些非常酷的功能 :)


泛型

如果您以前从未使用过类型系统,那么泛型可能很难理解,这就是为什么我想向您详细解释它。假设您要编写一个函数,该函数接受任何值作为参数,并将其作为返回值传递回去。您需要编写一个包含所有可能类型的联合体,或者使用any。这两种方法都不是好的解决方案,因为返回值的类型不正确。

type Primitive = string | number | boolean;

const identity = (
    x: Primitive | Array<Primitive> | Record<string, Primitive>,
) => x;

const test1 = identity('a'); // typeof test1: Primitive | Primitive[] | Record<string, Primitive>
const test2 = identity(1); // typeof test2: Primitive | Primitive[] | Record<string, Primitive>
Enter fullscreen mode Exit fullscreen mode

您需要对返回值执行类型缩小,以便以类型安全的方式使用它。any这将使您不必编写所有可能类型的联合,但会导致或多或少相同的结果:

const identity = (x: any) => x;
const test1 = identity('a'); // typeof test1: any
const test2 = identity(1); // typeof test2: any
Enter fullscreen mode Exit fullscreen mode

通用药物来救援!

const identity = <T>(x: T) => x;
const test1 = identity<string>('a'); // typeof test1: string
const test2 = identity<string>(1); // Argument of type 'number' is not assignable to parameter of type 'string'
const test3 = identity<number>(1); // typeof test3: number
const test4 = identity<boolean>(true); // typeof test4: boolean
Enter fullscreen mode Exit fullscreen mode

因为当我第一次看到这个语法时,我很难理解这里发生了什么,所以让我尝试用我自己的话来解释:

你想编写一个可供你的同事使用的工具函数,这个函数就是identity上面示例中的函数。对此有两种看法:

  • 您作为函数的编写者
  • 此实用功能的用户(您的同事)

首先,你需要先编写这个函数,然后你的同事才能使用它。这意味着,在你编写这个函数的时候,你并不知道你的同事会将哪种类型传递给它。可能是某种类型any😉。只有在使用某个参数调用该函数时,才能知道其类型。你的同事甚至可以依赖 TypeScript 的类型推断,而根本不需要指定类型:

const identity = <T>(x: T) => x;
const test1 = identity('a'); // typeof test1: "a"
const test2 = identity(1); // typeof test2: 1
const test3 = identity(true); // typeof test3: true
Enter fullscreen mode Exit fullscreen mode

这也带来了积极的副作用,那就是我们得到了更具体的类型。所以:

  • "a"而不是string
  • 1而不是number
  • true而不是boolean

太棒了!你还可以通过关键字限制输入extends。让我们看两个例子,看看如何限制身份函数只接受字符串或联合类型:

const identity = <T extends string>(x: T) => x;
const stringTest = identity('a'); // typeof stringTest: "a"
const numberTest = identity(1); // Argument of type 'number' is not assignable to parameter of type 'string'

// ---

const identity = <T extends 'A' | 'B' | 'C'>(x: T) => x;
const test1 = identity('A'); // typeof stringTest: "A"
const test2 = identity('D'); // Argument of type '"D"' is not assignable to parameter of type '"A" | "B" | "C"'
Enter fullscreen mode Exit fullscreen mode

现在,我们可以来看一个带有泛型参数和约束的函数的真实示例了。这是一个我在每个项目中都需要的工具函数,在下一节之后,你以后的每个项目中可能都会用到它。


定影Object.keys

我不知道你是否已经注意到了这一点,但是用于获取对象键的内置函数(Object.keys)的类型不正确。问题如下:

const obj = { a: 'a', b: 'b' };
type Obj = typeof obj; // { a: string; b: string; }
type Key = keyof Obj; // "a" | "b"

const keys = Object.keys(obj); // typeof keys: string[]
Enter fullscreen mode Exit fullscreen mode

我期望 的类型keys是:("a" | "b")[]。TypeScript 正确推断出单个键: ,但 的返回"a" | "b"类型似乎错误。现在我们知道问题所在了,我们可以尝试编写一个具有正确类型的包装函数:string[]Object.keys

const objectKeys = <T extends Record<string, unknown>>(obj: T) =>
    Object.keys(obj) as Array<keyof T>;

const obj = { a: 'a', b: 'b' };

const keys = objectKeys(obj); // typeof keys: ("a" | "b")[]
type Key = typeof keys[number]; // "a" | "b"
Enter fullscreen mode Exit fullscreen mode

这里发生了什么?我们创建了一个函数,它接受一个泛型类型的参数,但我们将其限制为对象类型。因此,如果您尝试传递string或 aArray作为参数,TypeScript 会报错。由于 TypeScript 具有非常出色的类型推断功能,它会知道只有ab是此对象的有效键,并将此类型返回给我们:("a" | "b")[]。如果您向对象添加一个c键,它会返回: ,("a" | "b" | "c")[]而无需对函数的实现进行任何更改,也无需自己编写类型。这就是泛型的强大之处。😍


类型安全omit函数

让我们用 4 次迭代来实现这一点,从简单的方法过渡到完全类型安全的方法。omit 函数的逻辑在所有 4 次迭代中都是相同的。我们只会更改类型。

幼稚的

const omit = (obj: Record<string, unknown>, keysToOmit: Array<string>) =>
    Object.fromEntries(
        Object.entries(obj).filter(([k]) => !keysToOmit.includes(k)),
    ) as Record<string, unknown>;

const obj = { a: 'a', b: 'b' };

omit(obj, ['c', '42']); // ['c', '42'] is a valid argument, but it should not be valid!

const partialObj = omit(obj, ['a']); // typeof partialObj: Record<string, unknown>
const a = partialObj.a; // typeof a: unknown
const b = partialObj.b; // typeof b: unknown
const c = partialObj.c; // typeof c: unknown
Enter fullscreen mode Exit fullscreen mode

在这种方法中,我们没有使用泛型。我们唯一支持的 TypeScript 是第一个参数必须是对象,第二个参数必须是字符串数组。返回值的类型是:Record<string, unknown>这基本上意味着:某个未知对象。a返回b类型被定义为unknown。如果我们尝试访问c输入中不存在的 ,我们也会得到unknown结果,并且不会出错。😔

Typescript 支持keysToOmit

const omit = <T extends Record<string, unknown>>(
    obj: T,
    keysToOmit: Array<keyof T>,
) =>
    Object.fromEntries(
        Object.entries(obj).filter(([k]) => !keysToOmit.includes(k)),
    ) as Record<string, unknown>;

const obj = { a: 'a', b: 'b' };

omit(obj, ['c']); // Type '"c"' is not assignable to type '"a" | "b"'

const partialObj = omit(obj, ['a']); // typeof partialObj: Record<string, unknown>
const a = partialObj.a; // typeof a: unknown
const b = partialObj.b; // typeof b: unknown
const c = partialObj.c; // typeof c: unknown
Enter fullscreen mode Exit fullscreen mode

现在我们使用了泛型,这样就可以为函数的使用者提供一些keysToOmit参数的智能感知。但返回值的类型仍然是:Record<string, unknown>。此外,我们仍然可以unknown得到abc。😔

返回值的类型

const omit = <T extends Record<string, unknown>>(
    obj: T,
    keysToOmit: Array<keyof T>,
) =>
    Object.fromEntries(
        Object.entries(obj).filter(([k]) => !keysToOmit.includes(k)),
    ) as Partial<T>;

const obj = { a: 'a', b: 'b' };

const partialObj = omit(obj, ['a']); // typeof partialObj: Partial<{a: string; b: string; }>
const a = partialObj.a; // typeof a: string | undefined
const b = partialObj.b; // typeof b: string | undefined
const c = partialObj.c; // Property 'c' does not exist on type 'Partial<{ a: string; b: string; }>'
Enter fullscreen mode Exit fullscreen mode

我们仍然保留了上次迭代中关于keysToOmit参数的改进,但现在还在as Partial<T>omit 函数的末尾添加了一个参数,这使得返回值的类型更加准确ab的类型string | undefined在某种程度上是正确的。但是当我们尝试访问 时,现在会报错c。仍然不完美。😔

类型安全方法

const omit = <T extends Record<string, unknown>, K extends Array<keyof T>>(
    obj: T,
    keysToOmit: K,
) =>
    Object.fromEntries(
        Object.entries(obj).filter(([k]) => !keysToOmit.includes(k)),
    ) as Omit<T, K[number]>;

const obj = { a: 'a', b: 'b' };

const partialObj = omit(obj, ['a']); // typeof partialObj: Omit<{ a: string; b: string; }, "a">
const a = partialObj.a; // Property 'a' does not exist on type 'Omit<{ a: string; b: string; }, "a">'
const b = partialObj.b; // typeof b: string
const c = partialObj.c; // Property 'c' does not exist on type 'Omit<{ a: string; b: string; }, "a">'
Enter fullscreen mode Exit fullscreen mode

现在看看这个。太棒了!你继承了之前迭代的所有优点,而且返回值的类型现在 100% 正确了。Onlyb是一个有效的键,并且它的类型string也是正确的。尝试访问a返回值会导致错误,因为它已被我们的函数移除。尝试访问c也会导致错误,因为它甚至不存在于输入对象中。😍


结束语

希望你学到了新东西,我很乐意收到反馈。好的👋

文章来源:https://dev.to/jarvispact/typescript-beyond-the-basics-2ap0
PREV
浏览器和 Node.js 中的事件循环有何区别?
NEXT
如何在最短时间内掌握一门新技术?循序渐进 参考文献 反馈