Эта статья расскажет о распространенных проблемах разработки на простых примерах, надеясь помочь вам лучше понять Typescript. Из-за введенияTypescriptЕсть много статей по базовым знаниям, да и сама официальная документация очень обширна.TypescriptОснова этой статьи не будет их повторять.
Зачем использовать машинопись?
Прежде чем начать текст, я хочу подумать над вопросом, зачем использоватьTypescript?
В отсутствиеTypescriptРаньше в большинстве проектов использовались нативныеJavascriptразработка. иJavascriptЭто «гибкий» язык по своей природе. «Гибкость» означает, что вы можете делать в коде все недобросовестное, например, брать числа и массивы для операций суммирования, вызывать методы, которые не существуют для объектов, передавать параметры, не соответствующие ожиданиям, в функции и т. д., и эти очевидные проблемы кодируются, сообщений об ошибках на этапе не будет.
const number = 1;
const arr = [1, 2, 3];
console.log(number + arr);
const obj = {};
obj.functionNotExist();
function pow2(value) {
return Math.pow(value, 2);
}
pow2("bazzzzzz");
В большом проекте тип «небольшого изменения» может привести к корректировке большого количества кода, что трудно обнаружить невооруженным глазом. Мы используемTypescriptОсновная цель состоит в том, чтобыбезопасность типа] (типобезопасный), используя объявления типов, чтобы предотвратить выполнение программами неправильных действий.
const number = 1;
const arr = [1, 2, 3];
console.log(number + arr); // 运算符“+”不能应用于类型“number”和“number[]”。
const obj = {};
obj.noExistFunction(); // 类型“{}”上不存在属性“noExistFunction”。
function pow(value: number) {
return Math.pow(value, 2);
}
pow("bazzzzzz"); // 类型“string”的参数不能赋给类型“number”的参数。
тонкая разница
существуетTypescriptМежду некоторыми ключевыми словами в концепциях есть небольшие различия, и их понимание может помочь вам писать более качественный код.
any vs unknown
anyпредставляет любой тип, который экранируетTypescriptпроверка типов и вJavascriptтак же, как вanyПеременная типа может выполнять любую операцию и компилироваться без ошибок.unknownтакже может представлять произвольные типы, но также сообщаетTypescriptРазработчики ничего об этом не знают, и им нужно быть осторожными при выполнении любых операций. Этот тип может выполнять только ограниченные операции (==、=== 、||、&&、?、!、typeof、instanceofд.), другие операции должныTypescriptДокажите, какого типа это значение, иначе будет возбуждено исключение.
let foo: any
let bar: unknown
foo.functionNotExist()
bar.functionNotExist() // 对象的类型为 "unknown"。
if (!!bar) { // ==、=== 、||、&&、?、!、typeof、instanceof
console.log(bar)
}
bar.toFixed(1) // Error
if (typeof bar=== 'number') {
bar.toFixed(1) // OK
}
anyЭто увеличивает риск ошибок во время выполнения, поэтому не используйте его без необходимости. Указывает, что он используется в сценарии [не знаю, какой тип]unknown.
{} vs object vs Object
objectозначает обычныйJavascriptТип объекта, а не примитивный тип данных.
declare function create(o: object): void;
create({ prop: 0 }); // OK
create(null); // Error
create(undefined); // Error
create(42); // Error
create("string"); // Error
create(false); // Error
create({
toString() {
return 3;
},
}); // OK
{}Представляет непустой, не неопределенный произвольный тип.
declare function create(o: {}): void;
create({ prop: 0 }); // OK
create(null); // Error
create(undefined); // Error
create(42); // OK
create("string"); // OK
create(false); // OK
create({
toString() {
return 3;
},
}); // OK
Objectи{}почти то же самое, разницаObjectтип будетObjectВстроенные методы прототипа (toString/hasOwnPreperty) Проверять.
declare function create(o: Object): void;
create({ prop: 0 }); // OK
create(null); // Error
create(undefined); // Error
create(42); // OK
create("string"); // OK
create(false); // OK
create({
toString() {
return 3;
},
}); // Error
Если требуется тип объекта, но свойства объекта не требуются, используйтеobject.{}иObjectЕсли сфера представления слишком широка, постарайтесь не использовать ее.
type vs interface
Оба могут использоваться для определения типов.
interface(Интерфейс) Могут быть объявлены только типы объектов, поддерживается слияние объявлений (расширяемое).
interface User {
id: string
}
interface User {
name: string
}
const user = {} as User
console.log(user.id);
console.log(user.name);
type(псевдоним типа) не поддерживает слияние объявлений, ведет себя какconst, letИмеет область действия блока.
type User = {
id: string,
}
if (true) {
type User = {
name: string,
}
const user = {} as User;
console.log(user.name);
console.log(user.id) // 类型“User”上不存在属性“id”。
}
typeВ более общем случае правая часть может быть любого типа, включая операции выражений, сопоставления типов, упомянутые ниже, и т. д.
type A = number
type B = A | string
type ValueOf<T> = T[keyof T];
Если вы разрабатываете пакет, модуль, разрешите другим расширять егоinterface, если вам нужно определить основные типы данных или требуются операции с типами, используйтеtype.
enum vs const enum
по умолчаниюenumбудет скомпилирован вJavascriptобъект и может бытьvalueОбратный поиск.
enum ActiveType {
active = 1,
inactive = 2,
}
function isActive(type: ActiveType) {}
isActive(ActiveType.active);
// ============================== compile result:
// var ActiveType;
// (function (ActiveType) {
// ActiveType[ActiveType["active"] = 1] = "active";
// ActiveType[ActiveType["inactive"] = 2] = "inactive";
// })(ActiveType || (ActiveType = {}));
// function isActive(type) { }
// isActive(ActiveType.active);
ActiveType[1]; // OK
ActiveType[10]; // OK!!!
cosnt enumне генерируется по умолчаниюJavascriptобъект, но напрямую выводит используемый кодvalue,не поддерживаетсяvalueОбратный поиск.
const enum ActiveType {
active = 1,
inactive = 2,
}
function isActive(type: ActiveType) {}
isActive(ActiveType.active);
// ============================== compile result:
// function isActive(type) { }
// isActive(1 /* active */);
ActiveType[1]; // Error
ActiveType[10]; // Error
enumТо, как значения индекса скобок подвержены ошибкам, относительноenum,const enumявляется более безопасным типом.
Режим сценария и режим модуля
TypescriptЕсть два режима, режим сценария (Script) один файл соответствует одномуhtmlизscriptметка, режим модуля (Module) следующий файл соответствуетTypescriptмодуль. Логика различия заключается в том, что пакет содержимого файла не содержитimportилиexportключевые слова.
Знание разницы между этими двумя режимами помогает понять некоторые «странности» при написании демонстрационного кода.
В режиме сценария все определения переменных и объявления типов являются глобальными, и если несколько файлов определяют одну и ту же переменную с одним и тем же именем, будет сообщено об ошибке.interfaceбудут объединены. В режиме модуля все определения переменных и объявления типов действительны внутри модуля.
Существуют также различия между двумя режимами при написании объявлений типов, таких как прямой режим сценария.declare var GlobalStoreВы можете написать объявление для глобального объекта.
GlobalStore.foo = "foo";
GlobalStore.bar = "bar"; // Error
declare var GlobalStore: {
foo: string;
};
В модульном режиме написание объявлений для глобальных объектов требуетdeclare global
GlobalStore.foo = "foo";
GlobalStore.bar = "bar";
declare global {
var GlobalStore: {
foo: string;
bar: string;
};
}
export {}; // export 关键字改变文件的模式
тип операции
Эта глава знакомитTypescriptОператоры общего типа в .
набор операций
&Представляет побитовый оператор AND в JS и используется в Typescript для вычисления пересечения двух типов.
type Type1 = "a" | "b";
type Type2 = "b" | "c";
type Type3 = Type1 & Type2; // 'b'
|Представляет побитовый оператор ИЛИ в JS и используется в Typescript для вычисления объединения двух типов.
type Type1 = "a" | "b";
type Type2 = "b" | "c";
type Type3 = Type1 | Type2; // 'a' 'b' 'c'
подпись индекса
Сигнатуры индексов можно использовать для определения типа свойств и значений внутри объекта, например, для определенияReactкомпоненты, позволяютPropsможет пройти любойkeyзаstring,valueзаnumberизprops.
interface Props {
[key: string]: number
}
<Component count={1} /> // OK
<Component count={true} /> // Error
<Component count={'1'} /> // Error
тип
допустимый типTypescriptИспользуйте типы, подобные объектам, которые принимают значения свойств.
type User = {
userId: string
friendList: {
fristName: string
lastName: string
}[]
}
type UserIdType = User['userId'] // string
type FriendList = User['friendList'] // { fristName: string; lastName: string; }[]
type Friend = FriendList[number] // { fristName: string; lastName: string; }
В приведенном выше примере мы используем функциональность набора текста изUserНесколько других типов рассчитываются в типе.FriendList[number]здесьnumberключевое слово, используемое для получения типа подэлемента массива. Вы также можете использовать буквенные числа в кортежах, чтобы получить типы элементов массива.
type Tuple = [number, string]
type First = Tuple[0] // number
type Second = Tuple[1] // string
typeof value
typeofКлючевое слово используется в JS для получения типа переменной, а результатом операции является строка (значение). Что представлено в TS, так это вывод типа (типа) переменной
let str1 = 'fooooo'
type Type1 = typeof str1 // type string
const str2 = 'fooooo'
type Type2 = typeof str2 // type "fooooo"
typeofИначе обстоит дело при вычислении переменных и констант, потому что константы не меняются, поэтомуTypescriptбудут использовать строгие типы, такие как следующиеType2пример,str2это строка типа 'fooooo'. Переменная будет иметь свободный строковый тип.
keyof Type
keyofключевое слово может использоваться для получения всего типа объектаkeyтип.
type User = {
id: string;
name: string;
};
type UserKeys = keyof User; //"id" | "name"
enumВ Typescript есть определенные особенности (иногда представляющие типы, но также и значения), если вы хотите получитьenumТип ключа, вам нужно сначала рассматривать его как значение, используйтеtypeofповторное использованиеkeyof.
enum ActiveType {
Active,
Inactive
}
type KeyOfType = keyof typeof ActiveType // "Active" | "Inactive"
extends
extendsКлючевые слова также имеют несколько применений, вinterfaceУказывает расширение типа в операторе условного типа, логическую операцию в операторе условного типа и играет ограничительную роль в универсальном типе.classуказывает на наследование.
// 表示类型扩展
interface A {
a: string
}
interface B extends A { // { a: string, b: string }
b: string
}
// 条件类型中起到布尔运算的功能
type Bar<T> = T extends string ? 'string' : never
type C = Bar<number> // never
type D = Bar<string> // string
type E = Bar<'fooo'> // string
// 起到类型限制的作用
type Foo<T extends object> = T
type F = Foo<number> // 类型“number”不满足约束“object”。
type G = Foo<string> // 类型“string”不满足约束“object”。
type H = Foo<{}> // OK
// 类继承
class I {}
class J extends I {}
сделатьA extends BУсловие, которое выполняется в булевой операции или общем ограничении,AдаBподмножество, то естьAнужно, чемBТочнее, по крайней мере, сBТакой же.
type K = '1' extends '1' | '2' ? 'true' : 'false' // "true"
type L = '1' | '2' extends '1' ? 'true' : 'false' // "false"
type M = { a: 1 } extends { a: 1, b: 1 } ? 'true' : 'false' // "false"
type N = { a: 1, b: 1 } extends { a: 1 } ? 'true' : 'false' // "true"
is
isключевое слово вTypescriptИспользуется в качестве защиты пользовательского типа, которую можно использовать, чтобы указать Typescript, как распознавать типы. Например, в следующем примере за методом isFish следуетpet is Fish, который сообщает Typescript, что когда метод возвращает true , это доказывает, что домашнее животное является типом рыбы, прошедшим аутентификацию пользователя, и может безопасно идентифицировать его какFish. Возврат false указывает на то, что питомец не является рыбой, и при использовании в качестве рыбы вы должны долго перекусывать.
interface Fish {
swim: () => {}
}
function isFish(pet: any): pet is Fish {
return (pet as Fish).swim !== undefined;
}
let pet = {} as unknown
if (isFish(pet)) {
pet.swim() // OK
} else {
pet.swim() // 类型“Bird”上不存在属性“swim”
}
Другими ключевыми словами, которые можно использовать для определения типа, являютсяtypeof,instanceof, inи Т. Д.
Дженерики
ДженерикиTypescriptочень важные точки знаний. Далее изfilterС чего начать знакомство с дженериками.
ПредположениеfilterМетод передает массив числовых типов и метод, который возвращает логическое значение, и, наконец, отфильтровывает желаемый результат и возвращает, объявление примерно выглядит следующим образом.
declare function filter(
array: number[],
fn: (item: unknown) => boolean
): number[];
Через некоторое время необходимо использоватьfilterметод для фильтрации некоторых строк, вы можете использоватьTypescriptФункция перегрузки функции, внутренний код фильтра остается без изменений, нужно только добавить определение типа.
declare function filter(
array: string[],
fn: (item: unknown) => boolean
): string[];
declare function filter(
array: number[],
fn: (item: unknown) => boolean
): number[];
Через некоторое время нужно использоватьfilterфильтроватьboolean[], фильтрobject[], отфильтруйте другие конкретные типы, если вы все еще используете перегруженные методы, будет много повторяющегося кода. В настоящее время вы можете рассмотреть возможность использования дженериков.Dont repeat yourself.
Дженерики похожиTypescript«Методы» в «языке» могут получать новые типы путем «передачи параметров». Обобщения, которые часто используются в повседневной разработке,Promise、Array、React.Componentи Т. Д.
Модернизация с помощью дженериковfilterметод:
declare function filter<T>(
array: T[],
fn: (item: unknown) => boolean
): T[];
Просто поставьте угловые скобки после имени метода<T>, указывающее, что метод поддерживает общий параметр (здесь T можно изменить на любое имя переменной, которое вам нравится, большинство людей предпочитают начинать с T, U, V...),array: T[]Указывает, что первый переданный параметр представляет собой массив общих типов шаблонов,:T[]Указывает, что метод возвращает массив шаблонного типа.TypescriptОн будет автоматически идентифицирован в соответствии с типом параметра.TТип, который фактически представлен, чтобы вы могли сохранить тип и избежать дублирования кода.
filter([1, 2, 3], () => true) // function filter<number>(array: number[], fn: (item: unknown) => boolean): number[]
filter(['1', '2', '3'], () => true) // function filter<string>(array: string[], fn: (item: unknown) => boolean): string[]
После сравнения дженериков с «методами» многие модели поведения становятся понятными. «Метод» может передавать параметры, может иметь несколько параметров, может иметь значения по умолчанию, а также могут использоваться дженерики.
type Foo<T, U = string> = { // 多参数、默认值
foo: Array<T> // 可以传递
bar: U
}
type A = Foo<number> // type A = { foo: number[]; bar: string; }
type B = Foo<number, number> // type B = { foo: number[]; bar: number; }
Не забывайте, что общие параметры также могут иметь ограничения, как в следующем примере.extendsэффект заключается в ограниченииTпо крайней мереHTMLElementтип.
type MyEvent<T extends HTMLElement = HTMLElement> = {
target: T,
type: string
}
Typescript поставляется с некоторыми общими инструментами, которые вводятся один за другим, а код реализации прилагается ниже.
тип карты
ключевое слово в
inКлючевые слова представляют сопоставления типов в типах, подобно тому, как пишутся сигнатуры индексов. В следующем примере объявляетсяPropsтип,keyтипа «количество» | тип «id»,valueзаnumberтип.
type Props = {
[key in 'count' | 'id']: number
}
const props1: Props = { // OK
count: 1,
id: 1
}
const props2: Props = {
count: '1', // ERROR
id: 1
}
const props3: Props = {
count: 1,
id: 1,
name: 1 // ERROR
}
Record
RecordОпределите тип ключа какKeys, тип значенияValuesтип объекта.
Пример:
enum ErrorCodes {
Timeout = 10001,
ServerBusy = 10002,
}
const ErrorMessageMap: Record<ErrorCodes, string> = {
[ErrorCodes.Timeout]: 'Timeout, please try again',
[ErrorCodes.ServerBusy]: 'Server is busy now'
}
Карты типов также можно использовать для комплексных проверок. Например, если в приведенном выше примере отсутствует ErrorCodes, Typescript также выдаст исключение.
enum ErrorCodes {
Timeout = 10001,
ServerBusy = 10002,
AuthFailed = 10003
}
// 类型 "{ 10001: string; 10002: string; }" 中缺少属性 "10003",但类型 "Record<ErrorCodes, string>" 中需要该属性
const ErrorMessageMap: Record<ErrorCodes, string> = {
[ErrorCodes.Timeout]: 'Timeout, please try again',
[ErrorCodes.ServerBusy]: 'Server is busy now'
}
Код:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
Partial
PartialАтрибуты определений типов можно сделать необязательными.
Пример:
type User = {
id?: string,
gender: 'male' | 'female'
}
type PartialUser = Partial<User> // { id?: string, gender?: 'male' | 'female'}
function createUser (user: PartialUser = { gender: 'male' }) {}
Userтип дляgenderАтрибуты обязательны (: у пользователя должен быть пол. А в оформленииcreateUserметод, для удобства программа выдастgenderДайте значение по умолчанию. В это время параметры могут быть изменены наPartial<User>, пользователь можетgender.
Код:
type Partial<T> = {
[U in keyof T]?: T[U];
};
Required
RequiredиPartialОбратный эффект заключается в том, что все свойства типа объекта становятся обязательными.
Пример:
type User = {
id?: string,
gender: 'male' | 'female'
}
type RequiredUser = Required<User> // { readonly id: string, readonly gender: 'male' | 'female'}
function showUserProfile (user: RequiredUser) {
console.log(user.id) // 不需要加 !
console.log(user.gender)
}
все еще используюUserтип,idЭто необязательно, когда атрибут определен (будет создан толькоid), при отображенииUser idОн должен уже существовать, и его можно использовать в это время.Required<User>, затем позвонитеshowUserProfileВремяUserВсе свойства должны бытьundefined.
Код:
type Required<T> = {
[U in keyof T]-?: T[U];
};
-?Символ здесь означает удаление необязательного символа?.
Readonly
ReadonlyСделать свойства типа объекта доступными только для чтения.
Пример:
type ReadonlyUser = Readonly<User> // { readonly id?: string, readonly gender: 'male' | 'female'}
const user: ReadonlyUser = {
id: '1',
gender: 'male'
}
user.gender = 'femail' // 无法分配到 "gender" ,因为它是只读属性。
Код:
type Readonly<T> = {
readonly [U in keyof T]: T[U];
};
Pick
Pick — это частичное свойство типа pick.
Пример:
type Location = {
latitude: number
longitude: number
city: string
address: string
province: string
district: string
}
type LatLong = Pick<Location, 'latitude' | 'longitude'> // { latitude: number; longitude: number; }
const region: LatLong = {
latitude: 22.545001,
longitude: 114.011712
}
ИмеетсяLocationвведите, и теперь нужны только данные широты и долготы, используйтеPick<Location, 'latitude' | 'longitude'>создать новоеLatLongтип.
Код:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
Omit
OmitКомбинированныйPickиExclude, частичные ключи в типах объектов игнорируются.
Пример:
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Omit<Todo, "description">; // { title: string; completed: boolean; }
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
Код:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Тип условия
Тернарный оператор
TypescriptОперации с типами также поддерживают «тернарные операторы», которые называются условными типами, обычно черезextendsКлючевое слово определяет, истинно условие или нет.Если оно истинно, оно получает тип, а если неверно, оно возвращает другой тип. Условные типы обычно появляются одновременно с дженериками (: потому что, если это известный фиксированный тип, нет необходимости судить.
type IsString<T> = T extends string ? true : false
type A = IsString<number> // false
type B = IsString<string> // true
Типы условий также имеют логику условного присваивания при работе с объединениями.number | stringВыполнение условной операции эквивалентноnumber 条件运算 | stringусловная операция
type ToArray<T> = T[]
type A = ToArray<number | string> // (string | number)[]
type ToArray2<T> = T extends unknown ? T[] : T[];
type B = ToArray2<number | string>; // string[] | number[]
infer
В дополнение к явному объявлению общих параметров,TypescriptТакже поддерживается динамическое выведение дженериков с использованиемinferключевые слова. В каких случаях требуется динамическая деривация? Обычно необходимо получить новый тип через входящий универсальный параметр, что отличается от прямого определения нового универсального параметра.
Например, теперь определитеApiResponseдва конкретных видаUserResponseиEventResponse, если вы хотите получитьUserтип сущности иEventЧто должен делать тип сущности?
type ApiResponse<T> = {
code: number
data: T
};
type UserResponse = ApiResponse<{
id: string,
name: string
}>
type EventResponse = ApiResponse<{
id: string,
title: string
}>
Конечно, вы можете взять его и определить новый тип отдельно.
type User = {
id: string,
name: string
}
type UserResponse = ApiResponse<User>
Но с этим непросто справиться, если тип предоставлен кем-то другим. В это время вы можете попробовать использоватьinfer, код показан ниже:
type ApiResponseEntity<T> = T extends ApiResponse<infer U> ? U : never;
type User = ApiResponseEntity<UserResponse>; // { id: string; name: string; }
type Event = ApiResponseEntity<EventResponse>; // { id: string; title: string; }
В примере определить тип входящегоTДа или нетT extends ApiResponse<infer U>подмножество , здесьinferоба пустьTypescriptпопытаться понятьTКакой типApiResponse, который генерирует новый общий параметрU. если удовлетворенextendsусловие будетUТип вернулся.
Хорошее понимание условных типов иinferПосле ключевого словаTypescriptВстроенный условный универсальный инструмент также хорошо изучен.
ReturnType
ReturntypeИспользуется для получения типа возвращаемого значения метода
Пример:
type A = (a: number) => string
type B = ReturnType<A> // string
Код:
type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any;
Parameters
ParametersТип параметра, используемый для получения метода
Пример:
type EventListenerParamsType = Parameters<typeof window.addEventListener>;
// [type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined]
Код:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any
? P : never;
Exclude
Excludeиспользуется для оценки типов в T, но не в U
Пример:
type A = number | string
type B = string
type C = Exclude<A, B> // number
Код:
type Exclude<T, U> = T extends U ? never : T;
Extract
Extractиспользуется для вычисления типов в T, которые могут быть присвоены U
Пример:
type A = number | string
type B = string
type C = Extract<A, B> // string
Код:
type Extract<T, U> = T extends U ? T : never;
NonNullable
Исключить из типаnullиundefined
Пример:
type A = {
a?: number | null
}
type B = NonNullable(A['a']) // number
Код:
type NonNullable<T> = T extends null | undefined ? never : T;
связанные с НПМ
TypescriptиnpmКак вы работаете вместе?
Как опубликовать пакет npm, написанный на Typescript?
Написание модулей npm на Typescript может предоставить пользователям файлы объявлений, чтобы компилятор мог обеспечить проверку типов и подсказки по коду при написании кода. Просто выполните следующие шаги:
- существует
tsconfig.jsonфайл, настроитьdeclaration: true, чтобы файл объявления автоматически генерировался каждый раз при компиляции Typescript. - существует
package.json, средняя конфигурацияprepublishOnlyscript компилирует код Typescript каждый раз, когда выполняется публикация npm; - регулировать
package.jsonсерединаmain,typesПоле указывает на окончательный путь кода
// tsconfig.json
{
"compilerOptions": {
"declaration": true // 自动生成声明文件 d.ts
}
}
// package.json
{
"name": "@scope/awesome-typescript-package",
"version": "0.0.1",
"main": "dist/index.js",
"types": "dist/index.d.ts", // 模块 types 路径
"scripts": {
"tsc": "tsc -p ./tsconfig.json",
"prepublishOnly": "npm run tsc" // 每次执行 npm publish 之前,编译代码
}
}
После успешного выпуска пользователи будут иметь правильные подсказки типов при импорте кода нашего модуля.
Объявление дополнительного типа расширенного трехстороннего модуля
Из-за популярности сообщества Typescript самые популярные пакеты npm будут иметь файлы объявления типов или предоставленные сообществом@typesСумкаDefinitelyTyped. Тем не менее, иногда встречаются пакеты, в которых отсутствуют определения типов, или пакеты, поддерживающие расширения плагинов, и в этих случаях нам необходимо добавить дополнительные объявления типов.
Сначала изменитьtsconfig.jsonсерединаcompilerOptions.types, добавьте наш собственный путь к файлу определенного типа. Например, все типы файлов находятся вtypesпуть, вы можете настроить"types": ["./types/*.d.ts"].
Затем в файле объявлений используйте функцию объединения объявлений для расширения соответствующего модуля.
// 扩展全局 jQuery,添加插件
declare global {
interface JQuery {
myPlugin: MyPlugin
}
}
// JSX 中扩展 Image 特性,可以在不更新版本的情况下使用 Web 新特性
import 'react'
declare module 'react' {
interface ImgHTMLAttributes<T> {
loading?: 'lazy2' | 'eager'
}
}
Обратите внимание, что здесь необходимо расширить тип, а не значение. Например, в react-native нативная сторона предоставляет свойства или методы стороне RN через модуль NativeModules (объект). Все его свойства по умолчанию имеют тип any.
import { NativeModules } from 'react-native'
NativeModules.MApplication.version // 默认情况下是 any 类型
Если вы хотите расширить тип объекта NativeModules, вам нужно перейти к исходному коду реактивного типа, чтобы найти его тип,NativeModulesStatic. Затем добавьте объявление типа в файл объявления.
import { NativeModules } from 'react-native'
declare module 'react-native' {
interface NativeModulesStatic {
MApplication: {
version: string
}
}
}
После добавления файла объявления Typescript может правильно определить тип.
import { NativeModules } from 'react-native'
NativeModules.MApplication.version // string
ВТФФФФФ?
В процессе использования Typescript я часто сталкиваюсь с каким-то странным поведением, здесь я записываю распространенные проблемы и пытаюсь объяснить почему.
Почему enum/class можно использовать как тип и значение?
использоватьenum/classВы смущены тем, почему они могут использоваться как для типов, так и для значений. Могут ли сами разработчики добиться такого же эффекта?
const enum ActiveType {
active = 1,
inactive = 2,
}
function isActive(type: ActiveType) {} // 类型
isActive(ActiveType.active); // 值
По факту,Typescriptподдерживать подобныесопутствующий объектшаблон, который связывает пары типа и объекта вместе. Пользователи могут импортировать оба вместе.
type Currency = {
unit: "EUR" | "GBP" | "JPY";
value: number;
};
const Currency = {
from: (value: number, unit: Currency["unit"] = "EUR"): Currency => {
return {
unit,
value: value,
};
},
};
const currency: Currency = Currency.from(10);
реализовать аналогичныйenumобъект воздействия
// enum-like.ts
const ActiveTypes = {
active: "active" as const,
inactive: "inactive" as const,
};
type ValueOf<T> = T[keyof T];
type ActiveTypes = ValueOf<typeof ActiveTypes>;
export { ActiveTypes };
// index.ts
import { ActiveTypes } from "./enum-like";
function isActive(value: ActiveTypes) {
console.log(value);
}
isActive(ActiveTypes.active);
isActive('zzzzzzzz'); // Error
Почему литерал сообщает об ошибке, но не после присвоения литерала
Предположим, мы теперь определяемrequestметод, этот метод поддерживает передачуOptionsтип параметра.
type Options = {
url: string;
method?: "get" | "post";
};
declare function request(options: Options): void;
request({
url: "https://juejin.com/api/user",
foo: "foo", // Error
});
const options1 = {
url: "https://juejin.com/api/user",
foo: "foo",
};
request(options1); // OK
По сравнению с другими методами записи вызовrequestМетод сообщит об исключении, если параметр передается непосредственно в виде литерала объекта, но объект присваивается временной переменной, а затем переменная передается вrequestметод, исключение исчезает. (С точки зрения типа Options, теоретически он в основном удовлетворяет{ url: string }Все типы могут быть назначены опциям. )
Причина в том, что "Typescript активирует [избыточную проверку свойств] для записи литералов объектов. Преимущество выполнения избыточной проверки свойств заключается в том, что это может помешать разработчикам дрожать и писать неправильные имена параметров.
Например, ниже разработчик ошибочноmethodпишется какmothed, если нет ошибки Typescript, проверить немного сложно (слова, написанные вами, радуют глаз).
request({
url: "https://juejin.com/api/user",
mothed: "post"
});
Если вам действительно нужно передать промежуточные переменные, вы можете указать тип при объявлении переменной, а также может иметь место избыточная проверка атрибутов.
const options2: Options = {
url: "https://juejin.com/api/user",
foo: "foo", // Error
};
Почему нельзя назначить «право» для типа «логическое | «правое» | «левое» | неопределенное»
Другим примером распространенной передачи литерала, приводящей к исключению типа, является поле 'fixed' при использовании компонента antd Table.
const columns = [
{
title: 'Available',
dataIndex: 'available',
fixed: 'right',
render: (value: boolean) => {
return value ? 'Y' : 'N'
},
},
]
return (
<Table
columns={columns} // 不能将类型“string”分配给类型“boolean | "right" | "left" | undefined”
dataSource={dataSource}
rowKey="id"
/>
}
найден послеcolumns, мы обнаруживаем, что строке 'right' нельзя присвоить тип "boolean | "right" | "left" | undefined". Причина в том, что Typescript будет выполнять [расширение типа] при выводе типа, намеренно получая более широкий тип. Исправление состоит в том, чтобы сообщить Typescript, что «право» здесь является константой, и не передавать его как строку.
// 方式一,加上 as const 类型断言
const columns = [
{
title: 'Available',
dataIndex: 'available',
fixed: 'right' as const, // right 是个常量
render: (value: boolean) => {
return value ? 'Y' : 'N'
},
},
]
// 方式二,给变量 columns 加上类型声明,避免 Typescript 自行推断
const columns: ColumnsType<Data> = [
{
title: 'Available',
dataIndex: 'available',
fixed: 'right'
render: (value: boolean) => {
return value ? 'Y' : 'N'
},
},
]
// 方式三,直接赋值给组件,不要中间值处理
<Table
columns={[
{
title: 'Available',
dataIndex: 'available',
fixed: 'right'
render: (value: boolean) => {
return value ? 'Y' : 'N'
},
},
]}
dataSource={dataSource}
rowKey="id"
/>
Почему Typescript не может различать типы объединения
Предположим, что теперь есть метод обработчика пользовательских событий, который поддерживает обработку двух типов событий:UserInputEvent valueдаstringтип,UserMouseEventда[number, number], при оценке типа event.value как строки интуитивно понятно, что цель должна быть HTMLInputElement, но на самом деле, когда вы продолжаете обращаться к цели в блоке кода if, обнаруживается, что Typescript не может различить тип цели .
type UserInputEvent = {
value: string;
target: HTMLInputElement;
};
type UserMouseEvent = {
value: [number, number];
target: HTMLElement;
};
type UserEvent = UserInputEvent | UserMouseEvent;
function handle(event: UserEvent) {
if (typeof event.value === "string") {
event.value; // string
event.target; // HTMLInputElement | UserMouseEvent
}
}
Причина в том, чтоA | BВ понимании Typescript это не только A или B, но и смешанный тип A и B. В следующем примере это может бытьCat | DogОбъекты типа имеют как свойство Cat, так и свойство Dog.
type Cat = {
name: string;
purrs: boolean;
};
type Dog = {
name: string;
barks: boolean;
};
// 并集类型中的某一个成员还可以同时属于每个成员
type CatOrDogOrBoth = Cat | Dog;
const a: CatOrDogOrBoth = { // OK
name: "foo",
purrs: false,
barks: false,
};
Чтобы правильно различать типы, вам нужно использовать литеральную (строковую, числовую, логическую и т. д.) нотацию, чтобы указать Typescript, что типы являются взаимоисключающими.
type UserInputEvent = {
type: "UserInputEvent";
value: string;
target: HTMLInputElement;
};
type UserMouseEvent = {
type: "UserMouseEvent";
value: [number, number];
target: HTMLElement;
};
type UserEvent = UserInputEvent | UserMouseEvent;
function handle(event: UserEvent) {
// 并集类型需要更加明确的推断
if (typeof event.value === "string") {
event.value; // string
event.target; // HTMLInputElement | UserMouseEvent
}
if (event.type === "UserMouseEvent") {
event.value; // string
event.target; // HTMLInputElement
}
}
Почему возникает ошибка в setTimeout после оценки типа?
Предположим, что теперь есть тип User с идентификатором undefined | string, вlogUserInfoОн судит о существовании user.id, но когда он продолжает выполняться в setTimeout, Typescript отклоняет его, думая, что id может не существовать.
type User = {
id?: string;
};
function logUserInfo(user: User) {
if (!user.id) {
return;
}
setTimeout(() => {
log(user.id); // 不能将类型“undefined”分配给类型“string”
});
}
function log(id: string) {
console.log(id);
}
Причина в том, что в цикле событий JS setTimeout и метод выполняются в macroTask, а предыдущее суждение!user.id находится не в том же стеке вызовов. пользователь ссылочного типа использоваться не будет. Таким образом, решение if (!user.id) игнорируется.
Решение состоит в том, чтобы добавить!утверждениеidнетundefined
setTimeout(() => {
log(user.id!);
});
Однако этот метод может привести к необходимости подтверждения (например, к необходимости использовать user.id).
type UserWithoutId = {};
type UserWithId = {
id: string;
};
type User = UserWithoutId | UserWithId;
function logUserInfo(user: User) {
if (!("id" in user)) {
return;
}
setTimeout(() => {
log(user.id);
});
}
function log(id: string) {
console.log(id);
}
Почему Typescript все еще сообщает об ошибке после написания утверждения типа as?
Часто используется при разработке TypescriptasЧтобы сделать утверждение типа (обычно как любое), эту операцию легко заставить разработчиков думать, что as является всемогущим, и тип может быть утвержден по желанию. На самом деле нет. Например, в следующем примере есть два типа Cat(имя, мурлыканье) и RobotCat(имя, серийный номер). мы хотим повторно использоватьsayCatNameметода (требуется передача типа Cat), сделайте утверждение типа для doraemon как Cat перед вызовом метода.
В это время Typescript выдаст исключение, и такое преобразование не разрешено!
type Cat = {
name: string;
purrs: boolean;
};
type RobotCat = {
name: string;
id: string;
}
function sayCatName (cat: Cat) {
console.log(cat.name)
}
const doraemon: RobotCat = {
id: '10000',
name: 'Doraemon'
}
sayCatName(doraemon as Cat) // 类型 "RobotCat" 中缺少属性 "purrs",但类型 "Cat" 中需要该属性
Причина в том, что утверждения типа могут использоваться только тогда, когда один тип является подтипом другого типа. Так как любое эфирное масло относится к тому же типу, любой другой тип может быть его подтипом, поэтомуany as Cat, Cat as anyВсе разрешены. иCatиRobotCatне совсем совпадает,TypescriptУтверждение этого типа считается небезопасным.
Когда понятно, что в коде не будет исключений, можно сделать два утверждения типа, чтобы избежать ошибок.
sayCatName(doraemon as unknown as Cat)
Лучший способ настроить ограничения типа метода, например, создать супертип Cat с типом Pick, чтобы Typescript проверял только имя свойства.
function sayCatName (cat: Pick<Cat, 'name'>) {
console.log(cat.name)
}
sayCatName(doraemon)
Эпилог
переучиваниеTypescriptВ процессе восполнения очков знаний, которые я раньше не замечал, я понял некоторые недоразумения в понимании, и в то же время я лучше понял некоторые «странные поведения» Typescript. Надеюсь, прочитав эту статью, вы также сможетеPartial<Typescript>прибытьTypescriptНа шаг ближе.
так какTypescriptСлишком много содержания и личных соображений автора, в статье могут быть огрехи, упущения, полная проработкаTypescriptПожалуйста, обратитесь к официальной документации.
Также спасибоTypescript 编程Эта книга многому меня научила.
Рекомендуемое чтение
- Онлайн-практика машинописного текстаtypescript-exercises.github.io/
- Программирование на TypeScriptitem.JD.com/12685323.Контракт…
- Малькольм доступен на e.com/today-i-…