предисловие
Typescript расширяет синтаксис и семантику типов для javascript, позволяя нам определять типы для переменных, функций и т. д., а затем проверять их во время компиляции, что может заранее обнаруживать ошибки несоответствия типов, а также может подсказывать доступные методы атрибутов во время разработки.
Кроме того, машинописный текст не меняет синтаксис, как тогдашний кофескрипт, это расширенный набор javascript, который только расширяет типы.
Эти преимущества быстро делают машинописный текст популярным. Если вы не знаете машинописный текст для предварительных интервью, вам может быть сложно получить предложение.
На рынке есть много обучающих статей о typescript, но ни одна из них не анализирует его реализацию с точки зрения принципа компиляции. В этой статье не будут рассказываться об основах машинописного текста, но будет реализована проверка типов машинописного текста, которая поможет вам понять, что делает проверка типов. После понимания идей реализации проверки типов изучение машинописного текста может оказаться не таким уж сложным.
Анализ мыслей
компилятор машинописного текста и Babel
Компилятор машинописного текста — это транспайлер, который отвечает за преобразование синтаксиса машинописного текста в целевой javascript es2015, es5 и es3, а также выполняет проверку типов в процессе.
Babel также является транспилером, который может преобразовывать es next, typescript, flow и другие грамматики в js, поддерживаемые целевой средой.
Может ли babel компилировать машинописный текст? Да, машинописный код может быть скомпилирован после Babel 7, что является результатом года сотрудничества между командой машинописного текста и командой Babel.
Мы знаем, что процесс компиляции Babel делится на три этапа: анализ, преобразование и генерация.
Фаза синтаксического анализа отвечает за компиляцию исходного кода в AST, фаза преобразования добавляет, удаляет и изменяет AST, а фаза генерации печатает AST в целевой код и создает исходную карту.
Babel может компилировать машинописный код, но он может его анализировать, но не будет выполнять проверку типов Мы можем реализовать проверку типов на основе AST, проанализированного Babel.
Что делает проверка типов
Мы часто используем tsc для проверки типов Вы когда-нибудь задумывались о том, что делает проверка типов?
что такое тип
Тип представляет содержимое, хранящееся в переменной, то есть указывает, сколько места в памяти занимает это содержимое и какие операции над ним можно выполнять. Например, number и boolean будут выделять разные байты памяти, а Date и String могут вызывать разные методы. Вот что делают типы. Он представляет собой возможность, сколько контента вы можете поместить в этот блок и что вы можете с ним делать.
Динамический тип означает, что тип определяется во время выполнения, а статический тип означает, что информация о типе переменной известна во время компиляции.С информацией о типе естественно знать, какие операции являются допустимыми, а какие недопустимыми.Да, что ему можно присвоить переменную.
Статическая типизация сохраняет информацию о типе в коде, которая может быть объявлена явно или автоматически. Если вы хотите сделать большой проект, если нет статического типа для ограничения и проверки кода заранее, слишком легко могут быть ошибки, и его будет сложно поддерживать. Это также причина, по которой машинописный текст и машинописный текст становятся все более и более популярными, поскольку интерфейсные проекты постепенно становятся все более сложными.
Как проверить тип
Мы знаем, что такое тип, зачем нужна статическая проверка типов и как ее проверить?
Проверка типа — это проверка содержимого переменной, а чтобы понять код, нужно разобрать код на AST, поэтому проверка типа становится проверкой структуры AST.
Например, если переменная объявлена как число, то присвоение ей строки является ошибкой типа.
Чтобы немного усложнить, если у типа есть универсальный тип, то есть есть параметры типа, то вам нужно передать конкретные параметры для определения типа, а затем сравнить их с фактическим AST после определения типа. .
Typescript также поддерживает расширенные типы, то есть типы могут выполнять различные операции, в этом случае вам нужно передать параметры типа, чтобы найти конкретный тип, а затем сравнить его с AST.
Напишем код для его реализации:
Код
Реализовать проверку типов для простых типов
Проверка типов для операторов присваивания
Например, в таком листе кода заявленное значение является строкой, но значение присваивается числу, очевидно, что есть неправильный тип, как мы можем проверить его ошибку.
let name: string;
name = 111;
Сначала мы используем babel для разбора этого кода в AST:
const parser = require('@babel/parser');
const sourceCode = `
let name: string;
name = 111;
`;
const ast = parser.parse(sourceCode, {
plugins: ['typescript']
});
Используйте парсер babel для разбора, включите плагин грамматики машинописного текста.
можно использоватьastexplerer.netчтобы увидеть его AST:
Реализовать проверку типов
Что нам нужно проверить, так это оператор присваивания AssignmentExpression, совпадают ли типы слева и справа.
Справа — числовой литерал NumericLiteral, по которому легко получить тип, а слева — ссылка, нужно получить объявляемый им тип из области видимости, а затем уже можно делать сравнение типов.
Babel предоставляет API-интерфейс области, который можно использовать для поиска объявления типа (привязки) в области видимости, а также может получить объявленный тип через getTypeAnnotation.
AssignmentExpression(path, state) {
const leftBinding = path.scope.getBinding(path.get('left'));
const leftType = leftBinding.path.get('id').getTypeAnnotation();// 左边的值声明的类型
}
Возвращаемый тип является объектом TSTypeAnnotation, нам нужно выполнить следующую обработку и преобразовать его в строку типа
Инкапсулируйте метод, передайте объект типа и возвращайте строки типа, такие как число, строка и т. д.
function resolveType(targetType) {
const tsTypeAnnotationMap = {
'TSStringKeyword': 'string'
}
switch (targetType.type) {
case 'TSTypeAnnotation':
return tsTypeAnnotationMap[targetType.typeAnnotation.type];
case 'NumberTypeAnnotation':
return 'number';
}
}
Таким образом, мы получаем типы слева и справа, а следующий шаг прост — путем сравнения мы знаем, совпадают ли типы:
AssignmentExpression(path, state) {
const rightType = resolveType(path.get('right').getTypeAnnotation());
const leftBinding = path.scope.getBinding(path.get('left'));
const leftType = resolveType(leftBinding.path.get('id').getTypeAnnotation());
if (leftType !== rightType ) {
// error: 类型不匹配
}
}
Ошибка оптимизации печати
Как распечатать сообщение об ошибке? Вы можете использовать @babel/code-frame, который поддерживает печать фрагмента выделенного кода.
path.get('right').buildCodeFrameError(`${rightType} can not assign to ${leftType}`, Error)
Эффект следующий:
Этот стек ошибок слишком уродлив, давайте удалим его и установим для Error.stackTraceLimit значение 0.
Error.stackTraceLimit = 0;
path.get('right').buildCodeFrameError(`${rightType} can not assign to ${leftType}`, Error));
Но после изменения здесь его нужно изменить обратно, то есть:
const tmp = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
console.log(path.get('right').buildCodeFrameError(`${rightType} can not assign to ${leftType}`, Error));
Error.stackTraceLimit = tmp;
Пробежимся снова:
Выглядит гораздо лучше!
сбор ошибок
Есть еще одна проблема: теперь он сообщает об ошибке, когда сталкивается с ошибкой типа, но мы надеемся собрать его, когда он встретит ошибку типа, и, наконец, единообразно сообщить об ошибке.
Как этого добиться? Где ошибка?
Файловый объект можно получить в плагине babel, а также есть методы set и get для доступа к некоторой глобальной информации. Вы можете получить файловый объект до и после вызова плагина, то есть на этапах pre и post (подробно они будут обсуждаться в буклете Nuggets «Коды для очистки плагинов Babel»).
Итак, мы можем сделать это:
pre(file) {
file.set('errors', []);
},
visitor: {
AssignmentExpression(path, state) {
const errors = state.file.get('errors');
const rightType = resolveType(path.get('right').getTypeAnnotation());
const leftBinding = path.scope.getBinding(path.get('left'));
const leftType = resolveType(leftBinding.path.get('id').getTypeAnnotation());
if (leftType !== rightType ) {
const tmp = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
errors.push(path.get('right').buildCodeFrameError(`${rightType} can not assign to ${leftType}`, Error));
Error.stackTraceLimit = tmp;
}
}
},
post(file) {
console.log(file.get('errors'));
}
Таким образом, ошибки могут быть собраны во время процесса и, наконец, напечатаны единообразно:
Таким образом, мы реализуем проверку типов для простых операторов присваивания.
Проверка типов для вызовов функций
Проверка операторов присваивания относительно проста.Давайте сделаем еще один шаг и реализуем проверку типов параметров вызова функции.
function add(a: number, b: number): number{
return a + b;
}
add(1, '2');
Здесь мы хотим проверить, согласуются ли параметры оператора вызова функции CallExpression с объявленными.
CallExpression состоит из двух частей: вызываемый и аргументы.Нам нужно найти объявление функции из области видимости в соответствии с вызываемым, а затем сравнить тип аргументов с типом параметров оператора объявления функции один за другим, чтобы реализовать тип проверка параметров вызова функции.
pre(file) {
file.set('errors', []);
},
visitor: {
CallExpression(path, state) {
const errors = state.file.get('errors');
// 调用参数的类型
const argumentsTypes = path.get('arguments').map(item => {
return resolveType(item.getTypeAnnotation());
});
const calleeName = path.get('callee').toString();
// 根据 callee 查找函数声明
const functionDeclarePath = path.scope.getBinding(calleeName).path;
// 拿到声明时参数的类型
const declareParamsTypes = functionDeclarePath.get('params').map(item => {
return resolveType(item.getTypeAnnotation());
})
argumentsTypes.forEach((item, index) => {
if (item !== declareParamsTypes[index]) {
// 类型不一致,报错
}
});
}
},
post(file) {
console.log(file.get('errors'));
}
Запускаем, эффект следующий:
Мы реализовали проверку типов для параметров вызова функции! На самом деле идея вполне ясна, и проверка других AST — аналогичная идея.
Реализовать проверку типов с помощью дженериков
То, что является универсальным типом, на самом деле является параметром типа, поэтому тип может определяться динамически в соответствии с входящими параметрами, а определение типа является более гибким.
Например этот кусок кода:
function add<T>(a: T, b: T) {
return a + b;
}
add<number>(1, '2');
Как сделать проверку типов?
Это все еще проверка типа оператора вызова функции, мы реализовали ее выше, но разница в том, что есть еще один параметр, тогда мы просто берем параметр типа и передаем его.
CallExpression(path, state) {
const realTypes = path.node.typeParameters.params.map(item => {// 先拿到类型参数的值,也就是真实类型
return resolveType(item);
});
const argumentsTypes = path.get('arguments').map(item => {
return resolveType(item.getTypeAnnotation());
});
const calleeName = path.get('callee').toString();
const functionDeclarePath = path.scope.getBinding(calleeName).path;
const realTypeMap = {};
functionDeclarePath.node.typeParameters.params.map((item, index) => {
realTypeMap[item.name] = realTypes[index];
});
const declareParamsTypes = functionDeclarePath.get('params').map(item => {
return resolveType(item.getTypeAnnotation(), realTypeMap);
})// 把类型参数的值赋值给函数声明语句的泛型参数
argumentsTypes.forEach((item, index) => { // 做类型检查的时候取具体的类型来对比
if (item !== declareParamsTypes[index]) {
// 报错,类型不一致
}
});
}
Еще один шаг в процессе определения конкретного типа универсального параметра.
Выполните, чтобы увидеть эффект:
Мы успешно поддержали проверку типов для операторов вызова функций с помощью дженериков!
Реализовать проверку типов для операторов вызова функций с расширенными типами.
Typescript поддерживает расширенные типы, то есть поддерживает различные операции над параметрами типа, а затем возвращает окончательный тип.
type Res<Param> = Param extends 1 ? number : string;
function add<T>(a: T, b: T) {
return a + b;
}
add<Res<1>>(1, '2');
Например, в этом коде Res — это расширенный тип, который возвращает новый тип после обработки входящего параметра типа Param.
Проверка типа этого оператора вызова функции немного сложнее, чем передача определенного типа универсального параметра.Сначала необходимо найти конкретный тип, затем передать параметры, а затем сравнить типы параметров.
Так как же оценивается этот продвинутый тип Res?
Давайте посмотрим на AST для этого типа Res:
Он имеет часть параметра типа (typeParameters) и часть логики вычисления определенного типа (typeAnnotation), правуюParam extends 1 ? number : string;Это оператор условия, где Params и 1 соответствуют checkType и extendsType соответственно, а число и строка соответствуют trueType и falseType соответственно.
Нам нужно только определить, равен ли входящий параметр 1, а затем мы можем узнать, является ли конкретный тип trueType или falseType.
Логика передачи параметров конкретных типов такая же, как и выше, поэтому не буду вдаваться в подробности, рассмотрим логику передачи значений по параметрам типа:
function typeEval(node, params) {
let checkType;
if(node.checkType.type === 'TSTypeReference') {
checkType = params[node.checkType.typeName.name];// 如果参数是泛型,则从传入的参数取值
} else {
checkType = resolveType(node.checkType); // 否则直接取字面量参数
}
const extendsType = resolveType(node.extendsType);
if (checkType === extendsType || checkType instanceof extendsType) { // 如果 extends 逻辑成立
return resolveType(node.trueType);
} else {
return resolveType(node.falseType);
}
}
Таким образом, мы можем найти окончательный тип расширенного типа этого Res, когда входящий Params равен 1.
Когда у вас есть окончательный тип, это то же самое, что проверка типа для вызова функции, которая напрямую передается конкретному типу. (Мы сделали это выше)
Выполните его, эффект будет следующим:
Полный код выглядит следующим образом (несколько длинный, вы можете пропустить его и посмотреть позже):
const { declare } = require('@babel/helper-plugin-utils');
function typeEval(node, params) {
let checkType;
if(node.checkType.type === 'TSTypeReference') {
checkType = params[node.checkType.typeName.name];
} else {
checkType = resolveType(node.checkType);
}
const extendsType = resolveType(node.extendsType);
if (checkType === extendsType || checkType instanceof extendsType) {
return resolveType(node.trueType);
} else {
return resolveType(node.falseType);
}
}
function resolveType(targetType, referenceTypesMap = {}, scope) {
const tsTypeAnnotationMap = {
TSStringKeyword: 'string',
TSNumberKeyword: 'number'
}
switch (targetType.type) {
case 'TSTypeAnnotation':
if (targetType.typeAnnotation.type === 'TSTypeReference') {
return referenceTypesMap[targetType.typeAnnotation.typeName.name]
}
return tsTypeAnnotationMap[targetType.typeAnnotation.type];
case 'NumberTypeAnnotation':
return 'number';
case 'StringTypeAnnotation':
return 'string';
case 'TSNumberKeyword':
return 'number';
case 'TSTypeReference':
const typeAlias = scope.getData(targetType.typeName.name);
const paramTypes = targetType.typeParameters.params.map(item => {
return resolveType(item);
});
const params = typeAlias.paramNames.reduce((obj, name, index) => {
obj[name] = paramTypes[index];
return obj;
},{});
return typeEval(typeAlias.body, params);
case 'TSLiteralType':
return targetType.literal.value;
}
}
function noStackTraceWrapper(cb) {
const tmp = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
cb && cb(Error);
Error.stackTraceLimit = tmp;
}
const noFuncAssignLint = declare((api, options, dirname) => {
api.assertVersion(7);
return {
pre(file) {
file.set('errors', []);
},
visitor: {
TSTypeAliasDeclaration(path) {
path.scope.setData(path.get('id').toString(), {
paramNames: path.node.typeParameters.params.map(item => {
return item.name;
}),
body: path.getTypeAnnotation()
});
path.scope.setData(path.get('params'))
},
CallExpression(path, state) {
const errors = state.file.get('errors');
const realTypes = path.node.typeParameters.params.map(item => {
return resolveType(item, {}, path.scope);
});
const argumentsTypes = path.get('arguments').map(item => {
return resolveType(item.getTypeAnnotation());
});
const calleeName = path.get('callee').toString();
const functionDeclarePath = path.scope.getBinding(calleeName).path;
const realTypeMap = {};
functionDeclarePath.node.typeParameters.params.map((item, index) => {
realTypeMap[item.name] = realTypes[index];
});
const declareParamsTypes = functionDeclarePath.get('params').map(item => {
return resolveType(item.getTypeAnnotation(), realTypeMap);
})
argumentsTypes.forEach((item, index) => {
if (item !== declareParamsTypes[index]) {
noStackTraceWrapper(Error => {
errors.push(path.get('arguments.' + index ).buildCodeFrameError(`${item} can not assign to ${declareParamsTypes[index]}`,Error));
});
}
});
}
},
post(file) {
console.log(file.get('errors'));
}
}
});
module.exports = noFuncAssignLint;
Вот и все, мы реализовали расширенный тип typescript!
Суммировать
Типы представляют собой содержимое переменных и операции, которые можно над ними выполнять.Статическая типизация позволяет выполнять проверку во время компиляции.По мере того, как фронтенд-проекты становятся все тяжелее и тяжелее, все чаще требуются статически типизированные языки, такие как машинописный текст.
Проверка типов заключается в сравнении AST, чтобы определить, соответствует ли объявление фактическому:
- Простые типы сравниваются напрямую, что эквивалентно if else
- С дженериками вы должны сначала передать параметр типа, чтобы определить тип, а затем сравнить, что эквивалентно обертыванию вызова функции, если еще
- Проверка типов дженериков с расширенными типами добавляет процесс оценки типов, что эквивалентно оценке if else после многоуровневых вызовов функций.
Реализовать полноценный чекер машинописного типа все же очень сложно, иначе код части машинописного чекера не будет состоять из десятков тысяч строк. Но идея на самом деле не такая уж и сложная, по идеям из нашей статьи можно реализовать полную проверку типов.
(Если вы не понимаете плагин Babel и API, вы можете узнать больше об этом в моем готовящемся буклете «Коды для очистки плагинов Babel». Если вы освоите Babel, вы также освоите возможности статического анализа, линтера, проверки типов. кстати можно и глубже освоить.)