feat(typescript): Support conditional types (#4007)

* feat(typescript): support for conditonal types

* refactor(js): reuse conditional expression logic

* chore(typescript): update snapshot for typescript conditional type test suite

* chore(js): make code support Node 4

* chore(js): rename utility functions

* chore(js): add comments for formatTernaryOperator

* fix(ts): support infer keyword

* chore(js): new line

* chore(js): improve readablity a little bit
master
Zhongliang Wang 2018-02-24 03:34:23 +08:00 committed by suchipi
parent 0b4d731fe5
commit d18da53e87
7 changed files with 207 additions and 107 deletions

View File

@ -55,7 +55,7 @@
"resolve": "1.5.0",
"semver": "5.4.1",
"string-width": "2.1.1",
"typescript": "2.7.0-insiders.20171214",
"typescript": "2.8.0-dev.20180222",
"typescript-eslint-parser": "14.0.0",
"unicode-regex": "1.0.1",
"unified": "6.1.6"

View File

@ -172,6 +172,135 @@ function hasJsxIgnoreComment(path) {
);
}
// The following is the shared logic for
// ternary operators, namely ConditionalExpression
// and TSConditionalType
function formatTernaryOperator(path, options, print, operatorOptions) {
const n = path.getValue();
const parts = [];
const operatorOpts = Object.assign(
{
beforeParts: () => [""],
afterParts: () => [""],
shouldCheckJsx: true,
operatorName: "ConditionalExpression",
consequentNode: "consequent",
alternateNode: "alternate",
testNode: "test"
},
operatorOptions || {}
);
// We print a ConditionalExpression in either "JSX mode" or "normal mode".
// See tests/jsx/conditional-expression.js for more info.
let jsxMode = false;
const parent = path.getParentNode();
let forceNoIndent = parent.type === operatorOpts.operatorName;
// Find the outermost non-ConditionalExpression parent, and the outermost
// ConditionalExpression parent. We'll use these to determine if we should
// print in JSX mode.
let currentParent;
let previousParent;
let i = 0;
do {
previousParent = currentParent || n;
currentParent = path.getParentNode(i);
i++;
} while (currentParent && currentParent.type === operatorOpts.operatorName);
const firstNonConditionalParent = currentParent || parent;
const lastConditionalParent = previousParent;
if (
(operatorOpts.shouldCheckJsx && isJSXNode(n[operatorOpts.testNode])) ||
isJSXNode(n[operatorOpts.consequentNode]) ||
isJSXNode(n[operatorOpts.alternateNode]) ||
conditionalExpressionChainContainsJSX(lastConditionalParent)
) {
jsxMode = true;
forceNoIndent = true;
// Even though they don't need parens, we wrap (almost) everything in
// parens when using ?: within JSX, because the parens are analogous to
// curly braces in an if statement.
const wrap = doc =>
concat([
ifBreak("(", ""),
indent(concat([softline, doc])),
softline,
ifBreak(")", "")
]);
// The only things we don't wrap are:
// * Nested conditional expressions in alternates
// * null
const isNull = node =>
node.type === "NullLiteral" ||
(node.type === "Literal" && node.value === null);
parts.push(
" ? ",
isNull(n[operatorOpts.consequentNode])
? path.call(print, operatorOpts.consequentNode)
: wrap(path.call(print, operatorOpts.consequentNode)),
" : ",
n[operatorOpts.alternateNode].type === operatorOpts.operatorName ||
isNull(n[operatorOpts.alternateNode])
? path.call(print, operatorOpts.alternateNode)
: wrap(path.call(print, operatorOpts.alternateNode))
);
} else {
// normal mode
const part = concat([
line,
"? ",
n[operatorOpts.consequentNode].type === operatorOpts.operatorName
? ifBreak("", "(")
: "",
align(2, path.call(print, operatorOpts.consequentNode)),
n[operatorOpts.consequentNode].type === operatorOpts.operatorName
? ifBreak("", ")")
: "",
line,
": ",
align(2, path.call(print, operatorOpts.alternateNode))
]);
parts.push(
parent.type === operatorOpts.operatorName
? options.useTabs
? dedent(indent(part))
: align(Math.max(0, options.tabWidth - 2), part)
: part
);
}
// In JSX mode, we want a whole chain of ConditionalExpressions to all
// break if any of them break. That means we should only group around the
// outer-most ConditionalExpression.
const maybeGroup = doc =>
jsxMode
? parent === firstNonConditionalParent ? group(doc) : doc
: group(doc); // Always group in normal mode.
// Break the closing paren to keep the chain right after it:
// (a
// ? b
// : c
// ).call()
const breakClosingParen =
!jsxMode && parent.type === "MemberExpression" && !parent.computed;
return maybeGroup(
concat(
[].concat(
operatorOpts.beforeParts(),
forceNoIndent ? concat(parts) : indent(concat(parts)),
operatorOpts.afterParts(breakClosingParen)
)
)
);
}
function printPathNoParens(path, options, print, args) {
const n = path.getValue();
const semi = options.semi ? ";" : "";
@ -1222,109 +1351,11 @@ function printPathNoParens(path, options, print, args) {
}
return concat(parts);
case "ConditionalExpression": {
// We print a ConditionalExpression in either "JSX mode" or "normal mode".
// See tests/jsx/conditional-expression.js for more info.
let jsxMode = false;
const parent = path.getParentNode();
let forceNoIndent = parent.type === "ConditionalExpression";
// Find the outermost non-ConditionalExpression parent, and the outermost
// ConditionalExpression parent. We'll use these to determine if we should
// print in JSX mode.
let currentParent;
let previousParent;
let i = 0;
do {
previousParent = currentParent || n;
currentParent = path.getParentNode(i);
i++;
} while (currentParent && currentParent.type === "ConditionalExpression");
const firstNonConditionalParent = currentParent || parent;
const lastConditionalParent = previousParent;
if (
isJSXNode(n.test) ||
isJSXNode(n.consequent) ||
isJSXNode(n.alternate) ||
conditionalExpressionChainContainsJSX(lastConditionalParent)
) {
jsxMode = true;
forceNoIndent = true;
// Even though they don't need parens, we wrap (almost) everything in
// parens when using ?: within JSX, because the parens are analogous to
// curly braces in an if statement.
const wrap = doc =>
concat([
ifBreak("(", ""),
indent(concat([softline, doc])),
softline,
ifBreak(")", "")
]);
// The only things we don't wrap are:
// * Nested conditional expressions in alternates
// * null
const isNull = node =>
node.type === "NullLiteral" ||
(node.type === "Literal" && node.value === null);
parts.push(
" ? ",
isNull(n.consequent)
? path.call(print, "consequent")
: wrap(path.call(print, "consequent")),
" : ",
n.alternate.type === "ConditionalExpression" || isNull(n.alternate)
? path.call(print, "alternate")
: wrap(path.call(print, "alternate"))
);
} else {
// normal mode
const part = concat([
line,
"? ",
n.consequent.type === "ConditionalExpression" ? ifBreak("", "(") : "",
align(2, path.call(print, "consequent")),
n.consequent.type === "ConditionalExpression" ? ifBreak("", ")") : "",
line,
": ",
align(2, path.call(print, "alternate"))
]);
parts.push(
parent.type === "ConditionalExpression"
? options.useTabs
? dedent(indent(part))
: align(Math.max(0, options.tabWidth - 2), part)
: part
);
}
// In JSX mode, we want a whole chain of ConditionalExpressions to all
// break if any of them break. That means we should only group around the
// outer-most ConditionalExpression.
const maybeGroup = doc =>
jsxMode
? parent === firstNonConditionalParent ? group(doc) : doc
: group(doc); // Always group in normal mode.
// Break the closing paren to keep the chain right after it:
// (a
// ? b
// : c
// ).call()
const breakClosingParen =
!jsxMode && parent.type === "MemberExpression" && !parent.computed;
return maybeGroup(
concat([
path.call(print, "test"),
forceNoIndent ? concat(parts) : indent(concat(parts)),
breakClosingParen ? softline : ""
])
);
}
case "ConditionalExpression":
return formatTernaryOperator(path, options, print, {
beforeParts: () => [path.call(print, "test")],
afterParts: breakClosingParen => [breakClosingParen ? softline : ""]
});
case "VariableDeclaration": {
const printed = path.map(childPath => {
return print(childPath);
@ -2872,6 +2903,25 @@ function printPathNoParens(path, options, print, args) {
case "PrivateName":
return concat(["#", path.call(print, "id")]);
case "TSConditionalType":
return formatTernaryOperator(path, options, print, {
beforeParts: () => [
path.call(print, "checkType"),
" ",
"extends",
" ",
path.call(print, "extendsType")
],
shouldCheckJsx: false,
operatorName: "TSConditionalType",
consequentNode: "trueType",
alternateNode: "falseType",
testNode: "checkType"
});
case "TSInferType":
return concat(["infer", " ", path.call(print, "typeParameter")]);
default:
/* istanbul ignore next */
throw new Error("unknown type: " + JSON.stringify(n.type));

View File

@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`conditonal-types.ts 1`] = `
export type DeepReadonly<T> = T extends any[] ? DeepReadonlyArray<T[number]> : T extends object ? DeepReadonlyObject<T> : T;
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
type DeepReadonlyObject<T> = {
readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export type DeepReadonly<T> = T extends any[]
? DeepReadonlyArray<T[number]>
: T extends object ? DeepReadonlyObject<T> : T;
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K
}[keyof T];
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
type DeepReadonlyObject<T> = {
readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>
};
`;
exports[`infer-type.ts 1`] = `
type TestReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
type TestReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: any;
`;

View File

@ -0,0 +1,9 @@
export type DeepReadonly<T> = T extends any[] ? DeepReadonlyArray<T[number]> : T extends object ? DeepReadonlyObject<T> : T;
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
type DeepReadonlyObject<T> = {
readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>;
};

View File

@ -0,0 +1 @@
type TestReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;

View File

@ -0,0 +1 @@
run_spec(__dirname, ["typescript"]);

View File

@ -4586,9 +4586,9 @@ typescript-eslint-parser@14.0.0:
lodash.unescape "4.0.1"
semver "5.5.0"
typescript@2.7.0-insiders.20171214:
version "2.7.0-insiders.20171214"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.0-insiders.20171214.tgz#841344ddae5f498a97c0435fcd12860480050e71"
typescript@2.8.0-dev.20180222:
version "2.8.0-dev.20180222"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.0-dev.20180222.tgz#50ee5fd5c76f2c9817e949803f946d74a559fc01"
uglify-es@3.0.28:
version "3.0.28"