Skip to content

Commit cd32221

Browse files
author
Kanchalai Tanglertsampan
committed
Check jsx attributes for intrinsic element to base on object-literal logic
1 parent cd6bc7f commit cd32221

11 files changed

+229
-128
lines changed

src/compiler/checker.ts

Lines changed: 96 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6868,9 +6868,14 @@ namespace ts {
68686868
// Use this property as the error node as this will be more helpful in
68696869
// reasoning about what went wrong.
68706870
Debug.assert(!!errorNode);
6871-
errorNode = prop.valueDeclaration;
6872-
reportError(Diagnostics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1,
6873-
symbolToString(prop), typeToString(target));
6871+
if (isJsxAttributes(errorNode)) {
6872+
reportError(Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(prop), typeToString(target));
6873+
}
6874+
else {
6875+
errorNode = prop.valueDeclaration;
6876+
reportError(Diagnostics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1,
6877+
symbolToString(prop), typeToString(target));
6878+
}
68746879
}
68756880
return true;
68766881
}
@@ -10700,18 +10705,17 @@ namespace ts {
1070010705
function isJsxAttribute(node: Node): node is JsxAttribute {
1070110706
return node.kind === SyntaxKind.JsxAttribute;
1070210707
}
10708+
1070310709
/**
10704-
* Resolee the type of attributes in the given attributes in opening-like element.
10705-
* Unlike "getJsxElementAttributesType" which get type of attributes type from
10706-
* resolving jsxopeningLikeElement's tagName
10710+
* Get attributes symbol of the given Jsx opening-like element. The result is from resolving "attributes" property of the opening-like element.
10711+
* @param openingLikeElement a Jsx opening-like element
10712+
* @return a symbol table resulted from resolving "attributes" property or undefined if any of the attribute resolved to any or there is no attributes.
1070710713
*/
10708-
function resolvedJsxAttributesTypeFromOpeningLikeElement(openingLikeElement: JsxOpeningLikeElement) {
10714+
function getAttributesSymbolTableOfJsxOpeningLikeElement(openingLikeElement: JsxOpeningElement): Map<Symbol> | undefined {
10715+
const attributes = openingLikeElement.attributes;
1070910716
let attributesTable = createMap<Symbol>();
10710-
let attributesArray: Symbol[] = [];
1071110717
const spreads: SpreadElementType[] = [];
10712-
10713-
const attributes = openingLikeElement.attributes;
10714-
10718+
let attributesArray: Symbol[] = [];
1071510719
for (const attributeDecl of attributes.properties) {
1071610720
const member = attributeDecl.symbol;
1071710721
if (isJsxAttribute(attributeDecl)) {
@@ -10732,25 +10736,29 @@ namespace ts {
1073210736
}
1073310737
attributeSymbol.type = exprType;
1073410738
attributeSymbol.target = member;
10735-
attributesTable[member.name] = member;
10736-
attributesArray.push(member);
10739+
attributesTable[attributeSymbol.name] = attributeSymbol;
10740+
attributesArray.push(attributeSymbol);
1073710741
}
1073810742
else {
1073910743
Debug.assert(attributeDecl.kind === SyntaxKind.JsxSpreadAttribute);
1074010744
if (attributesArray.length > 0) {
10741-
const t = createObjectLiteralType() as SpreadElementType;
10745+
const t = createJsxAttributesType(attributes, attributesTable) as SpreadElementType;
1074210746
t.isDeclaredProperty = true;
1074310747
spreads.push(t);
1074410748
attributesArray = [];
1074510749
attributesTable = createMap<Symbol>();
1074610750
}
10747-
spreads.push(checkExpression(attributeDecl.expression) as SpreadElementType);
10751+
const exprType = checkExpression(attributeDecl.expression);
10752+
if (isTypeAny(exprType)) {
10753+
return undefined;
10754+
}
10755+
spreads.push(exprType as SpreadElementType);
1074810756
}
1074910757
}
1075010758

1075110759
if (spreads.length > 0) {
1075210760
if (attributesArray.length > 0) {
10753-
const t = createObjectLiteralType() as SpreadElementType;
10761+
const t = createJsxAttributesType(attributes, attributesTable) as SpreadElementType;
1075410762
t.isDeclaredProperty = true;
1075510763
spreads.push(t);
1075610764
}
@@ -10764,13 +10772,47 @@ namespace ts {
1076410772
})
1076510773
}
1076610774

10767-
return createObjectLiteralType();
10768-
function createObjectLiteralType() {
10769-
const result = createAnonymousType(attributes.symbol, attributesTable, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined);
10770-
const freshObjectLiteralFlag = compilerOptions.suppressExcessPropertyErrors ? 0 : TypeFlags.FreshLiteral;
10771-
result.flags |= TypeFlags.ObjectLiteral | TypeFlags.ContainsObjectLiteral | freshObjectLiteralFlag;
10772-
return result;
10775+
return attributesTable;
10776+
}
10777+
10778+
/**
10779+
* Create ananoymous type from given attributes symbol table.
10780+
* @param jsxAttributes a JsxAttributes node containing attributes in attributesTable
10781+
* @param attributesTable a symbol table of attributes property
10782+
*/
10783+
function createJsxAttributesType(jsxAttributes: JsxAttributes, attributesTable: Map<Symbol>) {
10784+
const result = createAnonymousType(jsxAttributes.symbol, attributesTable, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined);
10785+
const freshObjectLiteralFlag = compilerOptions.suppressExcessPropertyErrors ? 0 : TypeFlags.FreshLiteral;
10786+
result.flags |= TypeFlags.ObjectLiteral | TypeFlags.ContainsObjectLiteral | freshObjectLiteralFlag;
10787+
return result;
10788+
}
10789+
10790+
function resolveCustomJsxElementAttributesTypeFromOpeningLikeElement(openingLikeElement: JsxOpeningLikeElement) {
10791+
const symbolTable = getAttributesSymbolTableOfJsxOpeningLikeElement(openingLikeElement);
10792+
return createJsxAttributesType(openingLikeElement.attributes, symbolTable);
10793+
}
10794+
10795+
/**
10796+
* Check attributes type of intrinsic JSx opening-like element.
10797+
* The function is intended to be called from checkJsxAttributes which has already check the the opening-like element is an intrinsic element.
10798+
* @param openingLikeElement an intrinsic Jsx opening-like element
10799+
*/
10800+
function checkAttributesTypeOfIntrinsicJsxOpeningLikeElement(openingLikeElement: JsxOpeningLikeElement) {
10801+
const targetAttributesType = getIntrinsicAttributesTypeFromJsxOpeningLikeElement(openingLikeElement);
10802+
const symbolTable = getAttributesSymbolTableOfJsxOpeningLikeElement(openingLikeElement);
10803+
// Filter out any hyphenated names as those are not play any role in type-checking unless there are corresponding properties in the target type
10804+
let attributesTable: Map<Symbol>;
10805+
let sourceAttributesType = anyType as Type;
10806+
if (symbolTable) {
10807+
attributesTable = createMap<Symbol>();
10808+
for (const key in symbolTable) {
10809+
if (isUnhyphenatedJsxName(key) || getPropertyOfType(targetAttributesType, key)) {
10810+
attributesTable[key] = symbolTable[key];
10811+
}
10812+
}
10813+
sourceAttributesType = createJsxAttributesType(openingLikeElement.attributes, attributesTable);
1077310814
}
10815+
checkTypeAssignableTo(sourceAttributesType, targetAttributesType, openingLikeElement.attributes.properties.length > 0 ? openingLikeElement.attributes : openingLikeElement);
1077410816
}
1077510817

1077610818
/**
@@ -10781,36 +10823,41 @@ namespace ts {
1078110823
function checkJsxAttributes(openingLikeElement: JsxOpeningLikeElement) {
1078210824
// Get target attributes type from resolving opening-element
1078310825
// Check if given attributes (openingLikeELement.attributes) are compatible with the given attributes
10784-
const targetAttributesType = getAttributesTypeFromJsxOpeningLikeElement(openingLikeElement);
10785-
const sourceAttribtuesType = resolvedJsxAttributesTypeFromOpeningLikeElement(openingLikeElement);
10786-
const nameTable = createMap<boolean>();
10787-
// Process this array in right-to-left order so we know which
10788-
// attributes (mostly from spreads) are being overwritten and
10789-
// thus should have their types ignored
10790-
let sawSpreadedAny = false;
10791-
const attributes = openingLikeElement.attributes.properties;
10792-
for (let i = attributes.length - 1; i >= 0; i--) {
10793-
if (attributes[i].kind === SyntaxKind.JsxAttribute) {
10794-
checkJsxAttribute(<JsxAttribute>(attributes[i]), targetAttributesType, nameTable);
10795-
}
10796-
else {
10797-
Debug.assert(attributes[i].kind === SyntaxKind.JsxSpreadAttribute);
10798-
const spreadType = checkJsxSpreadAttribute(<JsxSpreadAttribute>(attributes[i]), targetAttributesType, nameTable);
10799-
if (isTypeAny(spreadType)) {
10800-
sawSpreadedAny = true;
10826+
if (isJsxIntrinsicIdentifier(openingLikeElement.tagName)) {
10827+
checkAttributesTypeOfIntrinsicJsxOpeningLikeElement(openingLikeElement);
10828+
}
10829+
else {
10830+
const targetAttributesType = getAttributesTypeFromJsxOpeningLikeElement(openingLikeElement);
10831+
const sourceAttribtuesType = resolveCustomJsxElementAttributesTypeFromOpeningLikeElement(openingLikeElement);
10832+
const nameTable = createMap<boolean>();
10833+
// Process this array in right-to-left order so we know which
10834+
// attributes (mostly from spreads) are being overwritten and
10835+
// thus should have their types ignored
10836+
let sawSpreadedAny = false;
10837+
const attributes = openingLikeElement.attributes.properties;
10838+
for (let i = attributes.length - 1; i >= 0; i--) {
10839+
if (attributes[i].kind === SyntaxKind.JsxAttribute) {
10840+
checkJsxAttribute(<JsxAttribute>(attributes[i]), targetAttributesType, nameTable);
10841+
}
10842+
else {
10843+
Debug.assert(attributes[i].kind === SyntaxKind.JsxSpreadAttribute);
10844+
const spreadType = checkJsxSpreadAttribute(<JsxSpreadAttribute>(attributes[i]), targetAttributesType, nameTable);
10845+
if (isTypeAny(spreadType)) {
10846+
sawSpreadedAny = true;
10847+
}
1080110848
}
1080210849
}
10803-
}
1080410850

10805-
// Check that all required properties have been provided. If an 'any'
10806-
// was spreaded in, though, assume that it provided all required properties
10807-
if (targetAttributesType && !sawSpreadedAny) {
10808-
const targetProperties = getPropertiesOfType(targetAttributesType);
10809-
for (let i = 0; i < targetProperties.length; i++) {
10810-
if (!(targetProperties[i].flags & SymbolFlags.Optional) &&
10811-
!nameTable[targetProperties[i].name]) {
10851+
// Check that all required properties have been provided. If an 'any'
10852+
// was spreaded in, though, assume that it provided all required properties
10853+
if (targetAttributesType && !sawSpreadedAny) {
10854+
const targetProperties = getPropertiesOfType(targetAttributesType);
10855+
for (let i = 0; i < targetProperties.length; i++) {
10856+
if (!(targetProperties[i].flags & SymbolFlags.Optional) &&
10857+
!nameTable[targetProperties[i].name]) {
1081210858

10813-
error(openingLikeElement, Diagnostics.Property_0_is_missing_in_type_1, targetProperties[i].name, typeToString(targetAttributesType));
10859+
error(openingLikeElement, Diagnostics.Property_0_is_missing_in_type_1, targetProperties[i].name, typeToString(targetAttributesType));
10860+
}
1081410861
}
1081510862
}
1081610863
}
@@ -20390,6 +20437,7 @@ namespace ts {
2039020437

2039120438
function checkGrammarJsxElement(node: JsxOpeningLikeElement) {
2039220439
const seen = createMap<boolean>();
20440+
2039320441
for (const attr of node.attributes.properties) {
2039420442
if (attr.kind === SyntaxKind.JsxSpreadAttribute) {
2039520443
continue;

tests/baselines/reference/tsxAttributeErrors.errors.txt

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
tests/cases/conformance/jsx/tsxAttributeErrors.tsx(15,6): error TS2322: Type '42' is not assignable to type 'string'.
2-
tests/cases/conformance/jsx/tsxAttributeErrors.tsx(18,6): error TS2322: Type '"foo"' is not assignable to type 'number'.
3-
tests/cases/conformance/jsx/tsxAttributeErrors.tsx(22,6): error TS2606: Property 'text' of JSX spread attribute is not assignable to target property.
4-
Type 'number' is not assignable to type 'string'.
1+
tests/cases/conformance/jsx/tsxAttributeErrors.tsx(15,6): error TS2322: Type '{ text: 42; }' is not assignable to type '{ text?: string; width?: number; }'.
2+
Types of property 'text' are incompatible.
3+
Type '42' is not assignable to type 'string'.
4+
tests/cases/conformance/jsx/tsxAttributeErrors.tsx(18,6): error TS2322: Type '{ width: "foo"; }' is not assignable to type '{ text?: string; width?: number; }'.
5+
Types of property 'width' are incompatible.
6+
Type '"foo"' is not assignable to type 'number'.
7+
tests/cases/conformance/jsx/tsxAttributeErrors.tsx(22,6): error TS2322: Type '{ text: number; }' is not assignable to type '{ text?: string; width?: number; }'.
8+
Types of property 'text' are incompatible.
9+
Type 'number' is not assignable to type 'string'.
510

611

712
==== tests/cases/conformance/jsx/tsxAttributeErrors.tsx (3 errors) ====
@@ -21,19 +26,24 @@ tests/cases/conformance/jsx/tsxAttributeErrors.tsx(22,6): error TS2606: Property
2126
// Error, number is not assignable to string
2227
<div text={42} />;
2328
~~~~~~~~~
24-
!!! error TS2322: Type '42' is not assignable to type 'string'.
29+
!!! error TS2322: Type '{ text: 42; }' is not assignable to type '{ text?: string; width?: number; }'.
30+
!!! error TS2322: Types of property 'text' are incompatible.
31+
!!! error TS2322: Type '42' is not assignable to type 'string'.
2532

2633
// Error, string is not assignable to number
2734
<div width={'foo'} />;
2835
~~~~~~~~~~~~~
29-
!!! error TS2322: Type '"foo"' is not assignable to type 'number'.
36+
!!! error TS2322: Type '{ width: "foo"; }' is not assignable to type '{ text?: string; width?: number; }'.
37+
!!! error TS2322: Types of property 'width' are incompatible.
38+
!!! error TS2322: Type '"foo"' is not assignable to type 'number'.
3039

3140
// Error, number is not assignable to string
3241
var attribs = { text: 100 };
3342
<div {...attribs} />;
3443
~~~~~~~~~~~~
35-
!!! error TS2606: Property 'text' of JSX spread attribute is not assignable to target property.
36-
!!! error TS2606: Type 'number' is not assignable to type 'string'.
44+
!!! error TS2322: Type '{ text: number; }' is not assignable to type '{ text?: string; width?: number; }'.
45+
!!! error TS2322: Types of property 'text' are incompatible.
46+
!!! error TS2322: Type 'number' is not assignable to type 'string'.
3747

3848
// No errors here
3949
<span foo='bar' bar={'foo'} />;

0 commit comments

Comments
 (0)