1. Введение

 TypeScript представил два основных типа «никогда» и «неизвестно» в версиях 2.0 и 3.0 соответственно.После введения этих двух типов система типов TypeScript была значительно улучшена.

Но когда я обычно брался за код, я обнаружил, что концепция многих одноклассников все еще была в эре 1.0, эре, когда любой Дафа был хорошим. В конце концов, JavaScript — это динамический язык со слабой типизацией, и раньше мы не тратили слишком много времени на дизайн шрифтов. После введения TypeScript мы даже жаловались: «Почему этот код пишется все больше и больше?».

На самом деле, мы должны думать наоборот, парадигма ООП-программирования — это то, как должен выглядеть код после ES6.

2. Верхний и нижний шрифт в TypeScript

В дизайне системы типов есть два специальных типа:

  • Верхний тип: известный как универсальный супертип, то есть тип, который может содержать все значения.

  • Нижний тип: представляет тип без значения, он также известен как нулевой или нулевой тип и является подтипом всех типов.

Как объясняется системой типов, в TypeScript 3.0 есть два верхних типа (любой и неизвестный) и один нижний тип (никогда).

картина

Но некоторые люди думают, что любой также является нижним типом, потому что любой также может быть подтипом многих типов. Но это утверждение не является строгим, мы можем глубже взглянуть на три типа неизвестного, любое и никогда.

3. неизвестный и любой

3.1 неизвестно - представляет все

Когда я читаю код своих коллег, я редко вижу появление неизвестного типа. Это не значит, что это неважно, скорее это безопасная версия любого типа.

Разница между ним и любым очень проста, обратитесь к следующему примеру :

function format1(value: any) {
value.toFixed(2); // 不飘红,想干什么干什么,very dangerous
}

function format2(value: unknown) {
value.toFixed(2); // 代码会飘红,阻止你这么做

// 你需要收窄类型范围,例如:

// 1、类型断言 —— 不飘红,但执行时可能错误
(value as Number).toFixed(2);

// 2、类型守卫 —— 不飘红,且确保正常执行
if (typeof value === 'number') {
// 推断出类型: number
value.toFixed(2);
}

// 3、类型断言函数,抛出错误 —— 不飘红,且确保正常执行
assertIsNumber(value);
value.toFixed(2);
}


/** 类型断言函数,抛出错误 */
function assertIsNumber(arg: unknown): asserts arg is Number {
if (!(arg instanceof Number)) {
thrownewTypeError('Not a Number: ' + arg);
}
}

Использование любого похоже на исследование дома с привидениями, где призраки повсюду, когда код выполняется. Комбинация неизвестных и типовых охранников может гарантировать, что код может выполняться нормально, когда исходная структура данных неопределенна.

3.2 любой - голый

Когда мы используем любой, мы отказываемся от проверки типов, потому что он не используется для описания конкретного типа.

Прежде чем использовать его, нам нужно подумать о двух вещах:

  1. Можно ли использовать более конкретный тип

  2. Можете ли вы вместо этого использовать неизвестное

Во всех случаях любой является последним выбором.

3.3 Обзор конструкций предыдущего типа

В некоторых существующих дизайнах шрифтов используется любой, что недостаточно точно. Вот два примера:

3.3.1 Строка()

String() Может принимать любой параметр и преобразовывать его в строку.

Фактически, в сочетании с неизвестным типом, введенным выше, параметры здесь также могут быть спроектированы как неизвестные, но внутренняя реализация должна разработать больше защиты типа. Но неизвестный тип появился позже, поэтому в первоначальном дизайне все же использовалось любое, что мы и видим сейчас:

/**
* typescript/lib/lib.es5.d.ts
*/

interface StringConstructor {
new(value?: any): String;
(value?: any): string;
readonly prototype: String;
fromCharCode(...codes: number[]): string;
}

3.3.2 JSON.parse()

Недавно я написал фрагмент кода, который включает глубокое копирование:

exportfunction deleteCommentFromComments<T>(comments: GenericsComment<T>[], comment: GenericsComment<T>) {
// 深拷贝
const list: GenericsComment<T>[] = JSON.parse(JSON.stringify(comments));

// 找到对应的评论下标
const targetIndex = list.findIndex((item) => {
if (item.comment_id === comment.comment_id) {
returntrue;
}
returnfalse;
});

if (targetIndex !== -1) {
// 剔除对应的评论
list.splice(targetIndex, 1);
}

return list;
}

Очевидно, что выходные данные JSON.parse() динамически меняются вместе с входными данными (и могут даже вызвать ошибку), а сигнатура его функции разработана так:

interface JSON {
parse(text: string, reviver?: (this: any, key: string, value: any) =>any): any;
...
}

Могу ли я использовать здесь неизвестный? Да, но по той же причине, что и выше, JSON.parse() неизвестный тип не появлялся до того, как сигнатура функции была добавлена ​​в систему TypeScript, иначе возвращаемый тип должен быть неизвестным.

4. никогда

Как упоминалось выше, never тип представляет собой пустой тип, то есть тип, значение которого никогда не существует.

Две ситуации, когда значение никогда не будет существовать:

  1. Если при выполнении функции выдается исключение , для этой функции никогда не будет возвращаемого значения (поскольку выбрасывание исключения приведет к прямому прерыванию программы, из-за чего программа будет выполняться меньше, чем возвращаемое значение, то есть она имеет недостижимое значение). конечная точка, и она никогда не вернется. не существует и возвращается);

  2. Код, выполняющий бесконечный цикл ( infinite loop ) в функции, делает так, что программа никогда не дойдет до шага, на котором функция возвращает значение, и возврата никогда не будет.

// 异常
function err(msg: string): never { // OK
throw new Error(msg);
}

// 死循环
function loopForever(): never { // OK
while (true) {};
}

4.1 Уникальный тип дна

Поскольку never — единственный нижний тип машинописного текста, он может представлять подтип любого типа, поэтому его можно присвоить любому типу:

let err: never;
let num: number = 4;

num = err; // OK

картина

Мы можем использовать множества, чтобы понять никогда, неизвестное — это полное множество, никогда — это наименьшая единица (пустое множество), и любой тип содержит никогда.

4.1.1 null/undefined и никогда

Тут могут спросить.Похоже, что null и undefined тоже могут представлять подтипы любого типа.Почему не нижний тип. Нет, never является особенным в том, что, кроме самого себя, никакой другой тип не является его подтипом и не может быть ему присвоен. Это человек под человеком (голова собаки), мы можем использовать следующий пример для сравнения:

// null 和 undefined,可以被 never 赋值
declare const n: never;

let a: null = n; // 正确
let b: undefined = n; // 正确

// never 是 bottom type,除了自己以外没有任何类型可以赋值给它
let ne: never;

ne = null; // 错误
ne = undefined; // 错误

declare const an: any;
ne = an; // 错误,any 也不可以

declareconst nev: never;
ne = nev; // 正确,只有 never 可以赋值给 never

Вышеприведенный пример в основном иллюстрирует разницу между null/undefined и never, а never — нижняя.

4.1.2 Почему any не является строгим нижним типом

Когда я читал некоторые статьи, я обнаружил, что люди часто говорят, что любой является одновременно верхним и нижним типами, но это утверждение не является строгим.

Из вышеизложенного мы знаем, что никакому типу не может быть присвоено значение never, кроме самого never. Удовлетворяет ли какой-либо этой функции? Очевидно, что нет, если привести очень простой пример:

const a = 'anything';

const b: any = a; // 能够赋值
const c: never = a; // 报错,不能赋值

И почему мы говорим, что никогда не является нижним типом? Википедия объясняет это так:

Функция, тип возвращаемого значения которой является нижним (предположительно), не может возвращать никакое значение, даже единицу измерения нулевого размера, поэтому функция, тип возвращаемого значения которого является нижним, не может возвращать значение.

Функция, тип возвращаемого значения которой является нижним, не может возвращать никакое значение, даже единицу измерения нулевого размера. Таким образом, функция, возвращаемый тип которой является нижним, не может вернуться.

Отсюда мы также можем легко обнаружить, что в системе типов нижний тип уникален, и он однозначно описывает случай, когда функция не имеет возврата. Поэтому с никогда любая ересь без проверки типов — это точно не нижний тип.

4.2 Магия никогда

никогда не имеет следующих сценариев использования:

  • Проверка недостижимого кода: отметьте недостижимый код и получите подсказки по компиляции.

  • Типовая операция: как наименьший фактор в типовой операции.

  • Полная проверка: создайте подсказки компиляции для составных типов.

  • ......

Что касается использования never, на Zhihu есть хорошая дискуссия . Нельзя отрицать, что never — замечательная вещь. Далее мы подробно обсудим каждый вариант использования:

4.2.1 Проверка недостижимого кода

Новичок написал следующую строку кода:

process.exit(0);
console.log("hello world") // Unreachable code detected.ts(7027)

Не смейтесь, это действительно возможно. Конечно, если вы используете ts в это время, это даст вам подсказку компилятору:

Error: Unreachable code detected.ts(7027)

Поскольку  process.exit() тип возвращаемого значения определен как never, «недостижимый код» после него, естественно.

Другие возможные сценарии прослушивания сокетов:

function listen(): never {
while(true){
let conn = server.accept();
}
}

listen();
console.log("!!!"); // Unreachable code detected.ts(7027)

Обычно мы вручную помечаем возвращаемые функцией значения как never, чтобы помочь компилятору идентифицировать «недостижимый код» и помочь нам сузить тип. Вот немаркированный пример:

function throwError() {
throw new Error();
}

function firstChar(msg: string | undefined) {
if (msg === undefined)
throwError();
let chr = msg.charAt(1) // Object is possibly 'undefined'.
}

Поскольку компилятор не знает, что throwError является невозвратной функцией,  throwError() следующий код считается достижимым в любом случае, из-за чего компилятор неправильно понимает, что тип msg — string | undefined.

В это время, если отмечен тип never, тип msg будет сужен до строки после нулевой проверки:

function throwError(): never {
throw new Error();
}

function firstChar(msg: string | undefined) {
if (msg === undefined)
throwError();
let chr = msg.charAt(1) // ✅
}

4.2.2 Типовые операции

4.2.2.1 Минимальный коэффициент

Как упоминалось выше, никогда нельзя понимать пустое множество, тогда оно будет удовлетворять следующим правилам работы:

T | never => T
T & never =>never

То есть никогда не является наименьшим фактором операций с типами. Эти правила помогают нам упростить некоторые тривиальные операции с типами, например , такие как  Promise.race слияние нескольких  Promise, иногда невозможно узнать точное время и возвращаемый результат. Теперь мы используем один  Promise.race , чтобы объединить тот, который имеет возвращаемое значение сетевого запроса   , и  Promise другой, который будет возвращен в течение заданного времени   .rejectPromise

asyncfunction fetchNameWithTimeout(userId: string): Promise<string> {
const data = await Promise.race([
fetchData(userId),
timeout(3000)
])
return data.userName;
}

Ниже приведена реализация функции тайм-аута, которая выдает ошибку, если указанное время превышено. Поскольку он невозвратный, возвращаемый результат определяется как  Promise<never>:

function timeout(ms: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => reject(newError("Timeout!")), ms)
})
}

Отлично, тогда компилятор выведет  Promise.race возвращаемое значение, потому что race возьмет  Promise результат той, которая завершится первой, поэтому в приведенном выше примере сигнатура ее функции выглядит так:

function race<A, B>(inputs: [Promise<A>, Promise<B>]): Promise<A | B>

Замените в fetchData и timeout A на  { userName: string }, а B на  never. Следовательно,  promise тип возвращаемого значения вывода функции —  { userName: string } | never. И поскольку это  never наименьший фактор, его можно устранить. Таким образом, возвращаемое значение можно упростить до  { userName: string }, что нам и нужно.

Итак, что произойдет, если вы используете  any или  здесь unknown?

// 使用 any
function timeout(ms: number): Promise<any> {
......
}
// { userName: string } | any => any,失去了类型检查
asyncfunction fetchNameWithTimeout(userId: string): Promise<string> {
......
return data.userName; // ❌ data 被推断为 any
}

any легко понять.Хотя он может пройти нормально, это эквивалентно отсутствию проверки типов.

// 使用 unknown
function timeout(ms: number): Promise<unknown> {
......
}
// { userName: string } | unknown => unknown,类型被模糊
asyncfunction fetchNameWithTimeout(userId: string): Promise<string> {
......
return data.userName; // ❌ data 被推断为 unknown
}

unknown размывает тип и требует от нас сужения типа вручную.

Когда мы строго используем never для описания «недостижимого кода», компилятор может помочь нам точно сузить тип, чтобы код был документом.

4.2.2.2 Использование в типах условий

Мы часто встречаем never в условных типах, которые используются для представления случая else.

type Arguments<T> = T extends (...args: infer A) => any ? A : never
type Return<T> = T extends (...args: any[]) => infer R ? R : never

Для двух приведенных выше условных типов, которые выводят параметры функции и возвращаемые значения, даже если входящий T не является функциональным типом, мы можем получить подсказку от компилятора:

// Error: Type '3' is not assignable to type 'never'
const x: Return<"not a function type"> = 3;

Никогда также аккуратно играет свою роль минимального фактора при сужении типа объединения. Возьмем, к примеру, следующий пример T ,  исключающий null суммы  из undefined :

type NullOrUndefined = null | undefined
type NonNullable<T> = T extends NullOrUndefined ? never : T

// 运算过程
type NonNullable<string | null>
// 联合类型被分解成多个分支单独运算
=> (string extends NullOrUndefined ? never : string) | (nullextends NullOrUndefined ? never : null)
// 多个分支得到结果,再次联合
=> string | never
// never 在联合类型运算中被消解
=> string

4.2.3 Полная проверка

Составные типы, такие как типы объединения и алгебраические типы данных, можно сузить, комбинируя операторы switch:

interface Foo {
type: 'foo'
}

interface Bar {
type: 'bar'
}

type All = Foo | Bar;

function handleValue(val: All) {
switch (val.type) {
case'foo':
// val 此时是 Foo
break;
case'bar':
// val 此时是 Bar
break;
default:
// val 此时是 never
const exhaustiveCheck: never = val;
break;
}
}

Если кто-то позже  All изменит тип, он обнаружит ошибку компиляции:

type All = Foo | Bar | Baz;

function handleValue(val: All) {
switch (val.type) {
case'foo':
// val 此时是 Foo
break;
case'bar':
// val 此时是 Bar
break;
default:
// val 此时是 Baz
// ❌ Type 'Baz' is not assignable to type 'never'.(2322)
const exhaustiveCheck: never = val;
break;
}
}

В ветке по умолчанию значение val будет сужено до  Baz, в результате чего не будет присвоено значение never, что приведет к ошибке компиляции. Разработчики могут понять, что handleValue необходимо добавить логику обработки для Baz. Таким образом, вы можете гарантировать, что handleValue всегда исчерпывает все возможные типы All.

5. Вывод

Для студентов, которые ценят спецификацию типов и дизайн кода, TypeScript ни в коем случае не оковы, а прагматичный язык. Глубоко понимая использование и статус никогда и неизвестного в системе типов TypeScript, вы можете многое узнать о дизайне системы типов и теории множеств, разумно сузить типы в реальной разработке и организовать надежный и безопасный код.

В процессе обучения я также нашел очень хорошую электронную книгу по английскому языку, которую всем рекомендую: https://exploringjs.com/tackling-ts/toc.html

Справочная статья:

  1. https://blog.logrocket.com/when-to-use-never-and-unknown-in-typescript-5e4d6c5799ad/

  2. https://www.zhihu.com/question/354601204

картина
Следите за передовыми технологиями и углубляйтесь в профессиональные области
Отсканируйте код, чтобы подписаться на нас!
картина
картина