Skip to content

Commit

Permalink
Merge pull request #5185 from Microsoft/stringLiteralTypes
Browse files Browse the repository at this point in the history
String literal types
  • Loading branch information
DanielRosenwasser committed Nov 10, 2015
2 parents 92d37c3 + ea4e21d commit 9c28480
Show file tree
Hide file tree
Showing 108 changed files with 4,506 additions and 293 deletions.
83 changes: 64 additions & 19 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ namespace ts {
symbolToString,
getAugmentedPropertiesOfType,
getRootSymbols,
getContextualType,
getContextualType: getApparentTypeOfContextualType,
getFullyQualifiedName,
getResolvedSignature,
getConstantValue,
Expand Down Expand Up @@ -1653,7 +1653,7 @@ namespace ts {
writeAnonymousType(<ObjectType>type, flags);
}
else if (type.flags & TypeFlags.StringLiteral) {
writer.writeStringLiteral((<StringLiteralType>type).text);
writer.writeStringLiteral(`"${escapeString((<StringLiteralType>type).text)}"`);
}
else {
// Should never get here
Expand Down Expand Up @@ -4411,12 +4411,13 @@ namespace ts {
}

function getStringLiteralType(node: StringLiteral): StringLiteralType {
if (hasProperty(stringLiteralTypes, node.text)) {
return stringLiteralTypes[node.text];
const text = node.text;
if (hasProperty(stringLiteralTypes, text)) {
return stringLiteralTypes[text];
}

const type = stringLiteralTypes[node.text] = <StringLiteralType>createType(TypeFlags.StringLiteral);
type.text = getTextOfNode(node);
const type = stringLiteralTypes[text] = <StringLiteralType>createType(TypeFlags.StringLiteral);
type.text = text;
return type;
}

Expand Down Expand Up @@ -5744,6 +5745,10 @@ namespace ts {
return !!getPropertyOfType(type, "0");
}

function isStringLiteralType(type: Type) {
return type.flags & TypeFlags.StringLiteral;
}

/**
* Check if a Type was written as a tuple type literal.
* Prefer using isTupleLikeType() unless the use of `elementTypes` is required.
Expand Down Expand Up @@ -6976,7 +6981,7 @@ namespace ts {
else if (operator === SyntaxKind.BarBarToken) {
// When an || expression has a contextual type, the operands are contextually typed by that type. When an ||
// expression has no contextual type, the right operand is contextually typed by the type of the left operand.
let type = getContextualType(binaryExpression);
let type = getApparentTypeOfContextualType(binaryExpression);
if (!type && node === binaryExpression.right) {
type = checkExpression(binaryExpression.left);
}
Expand Down Expand Up @@ -7023,6 +7028,10 @@ namespace ts {
return applyToContextualType(type, t => getIndexTypeOfStructuredType(t, kind));
}

function contextualTypeIsStringLiteralType(type: Type): boolean {
return !!(type.flags & TypeFlags.Union ? forEach((<UnionType>type).types, isStringLiteralType) : isStringLiteralType(type));
}

// Return true if the given contextual type is a tuple-like type
function contextualTypeIsTupleLikeType(type: Type): boolean {
return !!(type.flags & TypeFlags.Union ? forEach((<UnionType>type).types, isTupleLikeType) : isTupleLikeType(type));
Expand All @@ -7048,7 +7057,7 @@ namespace ts {

function getContextualTypeForObjectLiteralElement(element: ObjectLiteralElement) {
const objectLiteral = <ObjectLiteralExpression>element.parent;
const type = getContextualType(objectLiteral);
const type = getApparentTypeOfContextualType(objectLiteral);
if (type) {
if (!hasDynamicName(element)) {
// For a (non-symbol) computed property, there is no reason to look up the name
Expand All @@ -7074,7 +7083,7 @@ namespace ts {
// type of T.
function getContextualTypeForElementExpression(node: Expression): Type {
const arrayLiteral = <ArrayLiteralExpression>node.parent;
const type = getContextualType(arrayLiteral);
const type = getApparentTypeOfContextualType(arrayLiteral);
if (type) {
const index = indexOf(arrayLiteral.elements, node);
return getTypeOfPropertyOfContextualType(type, "" + index)
Expand All @@ -7087,7 +7096,7 @@ namespace ts {
// In a contextually typed conditional expression, the true/false expressions are contextually typed by the same type.
function getContextualTypeForConditionalOperand(node: Expression): Type {
const conditional = <ConditionalExpression>node.parent;
return node === conditional.whenTrue || node === conditional.whenFalse ? getContextualType(conditional) : undefined;
return node === conditional.whenTrue || node === conditional.whenFalse ? getApparentTypeOfContextualType(conditional) : undefined;
}

function getContextualTypeForJsxExpression(expr: JsxExpression | JsxSpreadAttribute): Type {
Expand All @@ -7112,12 +7121,22 @@ namespace ts {

// Return the contextual type for a given expression node. During overload resolution, a contextual type may temporarily
// be "pushed" onto a node using the contextualType property.
function getContextualType(node: Expression): Type {
const type = getContextualTypeWorker(node);
function getApparentTypeOfContextualType(node: Expression): Type {
const type = getContextualType(node);
return type && getApparentType(type);
}

function getContextualTypeWorker(node: Expression): Type {
/**
* Woah! Do you really want to use this function?
*
* Unless you're trying to get the *non-apparent* type for a value-literal type,
* you probably meant to use 'getApparentTypeOfContextualType'.
* Otherwise this is slightly less useful.
*
* @param node the expression whose contextual type will be returned.
* @returns the contextual type of an expression.
*/
function getContextualType(node: Expression): Type {
if (isInsideWithStatementBody(node)) {
// We cannot answer semantic questions within a with block, do not proceed any further
return undefined;
Expand Down Expand Up @@ -7156,7 +7175,7 @@ namespace ts {
Debug.assert(parent.parent.kind === SyntaxKind.TemplateExpression);
return getContextualTypeForSubstitutionExpression(<TemplateExpression>parent.parent, node);
case SyntaxKind.ParenthesizedExpression:
return getContextualType(<ParenthesizedExpression>parent);
return getApparentTypeOfContextualType(<ParenthesizedExpression>parent);
case SyntaxKind.JsxExpression:
case SyntaxKind.JsxSpreadAttribute:
return getContextualTypeForJsxExpression(<JsxExpression>parent);
Expand Down Expand Up @@ -7196,7 +7215,7 @@ namespace ts {
Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node));
const type = isObjectLiteralMethod(node)
? getContextualTypeForObjectLiteralMethod(node)
: getContextualType(node);
: getApparentTypeOfContextualType(node);
if (!type) {
return undefined;
}
Expand Down Expand Up @@ -7326,7 +7345,7 @@ namespace ts {
type.pattern = node;
return type;
}
const contextualType = getContextualType(node);
const contextualType = getApparentTypeOfContextualType(node);
if (contextualType && contextualTypeIsTupleLikeType(contextualType)) {
const pattern = contextualType.pattern;
// If array literal is contextually typed by a binding pattern or an assignment pattern, pad the resulting
Expand Down Expand Up @@ -7418,7 +7437,7 @@ namespace ts {

const propertiesTable: SymbolTable = {};
const propertiesArray: Symbol[] = [];
const contextualType = getContextualType(node);
const contextualType = getApparentTypeOfContextualType(node);
const contextualTypeHasPattern = contextualType && contextualType.pattern &&
(contextualType.pattern.kind === SyntaxKind.ObjectBindingPattern || contextualType.pattern.kind === SyntaxKind.ObjectLiteralExpression);
let typeFlags: TypeFlags = 0;
Expand Down Expand Up @@ -9419,7 +9438,12 @@ namespace ts {
const targetType = getTypeFromTypeNode(node.type);
if (produceDiagnostics && targetType !== unknownType) {
const widenedType = getWidenedType(exprType);
if (!(isTypeAssignableTo(targetType, widenedType))) {

// Permit 'number[] | "foo"' to be asserted to 'string'.
const bothAreStringLike =
someConstituentTypeHasKind(targetType, TypeFlags.StringLike) &&
someConstituentTypeHasKind(widenedType, TypeFlags.StringLike);
if (!bothAreStringLike && !(isTypeAssignableTo(targetType, widenedType))) {
checkTypeAssignableTo(exprType, targetType, node, Diagnostics.Neither_type_0_nor_type_1_is_assignable_to_the_other);
}
}
Expand Down Expand Up @@ -10249,6 +10273,10 @@ namespace ts {
case SyntaxKind.ExclamationEqualsToken:
case SyntaxKind.EqualsEqualsEqualsToken:
case SyntaxKind.ExclamationEqualsEqualsToken:
// Permit 'number[] | "foo"' to be asserted to 'string'.
if (someConstituentTypeHasKind(leftType, TypeFlags.StringLike) && someConstituentTypeHasKind(rightType, TypeFlags.StringLike)) {
return booleanType;
}
if (!isTypeAssignableTo(leftType, rightType) && !isTypeAssignableTo(rightType, leftType)) {
reportOperatorError();
}
Expand Down Expand Up @@ -10387,6 +10415,15 @@ namespace ts {
return getUnionType([type1, type2]);
}

function checkStringLiteralExpression(node: StringLiteral): Type {
const contextualType = getContextualType(node);
if (contextualType && contextualTypeIsStringLiteralType(contextualType)) {
return getStringLiteralType(node);
}

return stringType;
}

function checkTemplateExpression(node: TemplateExpression): Type {
// We just want to check each expressions, but we are unconcerned with
// the type of each expression, as any value may be coerced into a string.
Expand Down Expand Up @@ -10446,7 +10483,7 @@ namespace ts {
if (isInferentialContext(contextualMapper)) {
const signature = getSingleCallSignature(type);
if (signature && signature.typeParameters) {
const contextualType = getContextualType(<Expression>node);
const contextualType = getApparentTypeOfContextualType(<Expression>node);
if (contextualType) {
const contextualSignature = getSingleCallSignature(contextualType);
if (contextualSignature && !contextualSignature.typeParameters) {
Expand Down Expand Up @@ -10517,6 +10554,7 @@ namespace ts {
case SyntaxKind.TemplateExpression:
return checkTemplateExpression(<TemplateExpression>node);
case SyntaxKind.StringLiteral:
return checkStringLiteralExpression(<StringLiteral>node);
case SyntaxKind.NoSubstitutionTemplateLiteral:
return stringType;
case SyntaxKind.RegularExpressionLiteral:
Expand Down Expand Up @@ -12711,6 +12749,7 @@ namespace ts {
let hasDuplicateDefaultClause = false;

const expressionType = checkExpression(node.expression);
const expressionTypeIsStringLike = someConstituentTypeHasKind(expressionType, TypeFlags.StringLike);
forEach(node.caseBlock.clauses, clause => {
// Grammar check for duplicate default clauses, skip if we already report duplicate default clause
if (clause.kind === SyntaxKind.DefaultClause && !hasDuplicateDefaultClause) {
Expand All @@ -12731,6 +12770,12 @@ namespace ts {
// TypeScript 1.0 spec (April 2014):5.9
// In a 'switch' statement, each 'case' expression must be of a type that is assignable to or from the type of the 'switch' expression.
const caseType = checkExpression(caseClause.expression);

// Permit 'number[] | "foo"' to be asserted to 'string'.
if (expressionTypeIsStringLike && someConstituentTypeHasKind(caseType, TypeFlags.StringLike)) {
return;
}

if (!isTypeAssignableTo(expressionType, caseType)) {
// check 'expressionType isAssignableTo caseType' failed, try the reversed check and report errors if it fails
checkTypeAssignableTo(caseType, expressionType, caseClause.expression, /*headMessage*/ undefined);
Expand Down
8 changes: 5 additions & 3 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1992,9 +1992,7 @@ namespace ts {

function parseParameterType(): TypeNode {
if (parseOptional(SyntaxKind.ColonToken)) {
return token === SyntaxKind.StringLiteral
? <StringLiteral>parseLiteralNode(/*internName*/ true)
: parseType();
return parseType();
}

return undefined;
Expand Down Expand Up @@ -2382,6 +2380,8 @@ namespace ts {
// If these are followed by a dot, then parse these out as a dotted type reference instead.
const node = tryParse(parseKeywordAndNoDot);
return node || parseTypeReferenceOrTypePredicate();
case SyntaxKind.StringLiteral:
return <StringLiteral>parseLiteralNode(/*internName*/ true);
case SyntaxKind.VoidKeyword:
case SyntaxKind.ThisKeyword:
return parseTokenNode<TypeNode>();
Expand Down Expand Up @@ -2412,6 +2412,7 @@ namespace ts {
case SyntaxKind.OpenBracketToken:
case SyntaxKind.LessThanToken:
case SyntaxKind.NewKeyword:
case SyntaxKind.StringLiteral:
return true;
case SyntaxKind.OpenParenToken:
// Only consider '(' the start of a type if followed by ')', '...', an identifier, a modifier,
Expand Down Expand Up @@ -5629,6 +5630,7 @@ namespace ts {
return parseTokenNode<JSDocType>();
}

// TODO (drosen): Parse string literal types in JSDoc as well.
return parseJSDocTypeReference();
}

Expand Down
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ namespace ts {
}

// Note that a StringLiteral AST node is both an Expression and a TypeNode. The latter is
// because string literals can appear in the type annotation of a parameter node.
// because string literals can appear in type annotations as well.
export interface StringLiteral extends LiteralExpression, TypeNode {
_stringLiteralBrand: any;
}
Expand Down
16 changes: 8 additions & 8 deletions tests/baselines/reference/callSignatureFunctionOverload.types
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
=== tests/cases/compiler/callSignatureFunctionOverload.ts ===
var foo: {
>foo : { (name: string): string; (name: 'order'): string; (name: 'content'): string; (name: 'done'): string; }
>foo : { (name: string): string; (name: "order"): string; (name: "content"): string; (name: "done"): string; }

(name: string): string;
>name : string

(name: 'order'): string;
>name : 'order'
>name : "order"

(name: 'content'): string;
>name : 'content'
>name : "content"

(name: 'done'): string;
>name : 'done'
>name : "done"
}

var foo2: {
>foo2 : { (name: string): string; (name: 'order'): string; (name: 'order'): string; (name: 'done'): string; }
>foo2 : { (name: string): string; (name: "order"): string; (name: "order"): string; (name: "done"): string; }

(name: string): string;
>name : string

(name: 'order'): string;
>name : 'order'
>name : "order"

(name: 'order'): string;
>name : 'order'
>name : "order"

(name: 'done'): string;
>name : 'done'
>name : "done"
}

16 changes: 8 additions & 8 deletions tests/baselines/reference/constantOverloadFunction.types
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,27 @@ class Derived3 extends Base { biz() { } }
>biz : () => void

function foo(tagName: 'canvas'): Derived1;
>foo : { (tagName: 'canvas'): Derived1; (tagName: 'div'): Derived2; (tagName: 'span'): Derived3; (tagName: string): Base; }
>tagName : 'canvas'
>foo : { (tagName: "canvas"): Derived1; (tagName: "div"): Derived2; (tagName: "span"): Derived3; (tagName: string): Base; }
>tagName : "canvas"
>Derived1 : Derived1

function foo(tagName: 'div'): Derived2;
>foo : { (tagName: 'canvas'): Derived1; (tagName: 'div'): Derived2; (tagName: 'span'): Derived3; (tagName: string): Base; }
>tagName : 'div'
>foo : { (tagName: "canvas"): Derived1; (tagName: "div"): Derived2; (tagName: "span"): Derived3; (tagName: string): Base; }
>tagName : "div"
>Derived2 : Derived2

function foo(tagName: 'span'): Derived3;
>foo : { (tagName: 'canvas'): Derived1; (tagName: 'div'): Derived2; (tagName: 'span'): Derived3; (tagName: string): Base; }
>tagName : 'span'
>foo : { (tagName: "canvas"): Derived1; (tagName: "div"): Derived2; (tagName: "span"): Derived3; (tagName: string): Base; }
>tagName : "span"
>Derived3 : Derived3

function foo(tagName: string): Base;
>foo : { (tagName: 'canvas'): Derived1; (tagName: 'div'): Derived2; (tagName: 'span'): Derived3; (tagName: string): Base; }
>foo : { (tagName: "canvas"): Derived1; (tagName: "div"): Derived2; (tagName: "span"): Derived3; (tagName: string): Base; }
>tagName : string
>Base : Base

function foo(tagName: any): Base {
>foo : { (tagName: 'canvas'): Derived1; (tagName: 'div'): Derived2; (tagName: 'span'): Derived3; (tagName: string): Base; }
>foo : { (tagName: "canvas"): Derived1; (tagName: "div"): Derived2; (tagName: "span"): Derived3; (tagName: string): Base; }
>tagName : any
>Base : Base

Expand Down
Loading

0 comments on commit 9c28480

Please sign in to comment.