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 bitmaster
parent
0b4d731fe5
commit
d18da53e87
|
@ -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"
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
||||
`;
|
|
@ -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]>;
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
type TestReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;
|
|
@ -0,0 +1 @@
|
|||
run_spec(__dirname, ["typescript"]);
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue