import * as ts from 'typescript' ; import * as tsutils from 'tsutils/typeguard/3.0' ; import { VisitorContext } from './visitor-context' ; import * as VisitorUtils from './visitor-utils' ; import * as VisitorIsNumber from './visitor-is-number' ; import * as VisitorIsString from './visitor-is-string' ; import * as VisitorTypeCheck from './visitor-type-check' ; import * as VisitorTypeName from './visitor-type-name' ; import { sliceSet } from './utils' ; function visitRegularObjectType ( type , indexType , visitorContext ) { const name = VisitorTypeName . visitType ( type , visitorContext , { type : 'indexed-access' , indexType } ) ; return VisitorUtils . setFunctionIfNotExists ( name , visitorContext , ( ) => { const properties = visitorContext . checker . getPropertiesOfType ( type ) ; const propertiesInfo = properties . map ( ( property ) => VisitorUtils . getPropertyInfo ( type , property , visitorContext ) ) ; const stringType = VisitorIsString . visitType ( indexType , visitorContext ) ; if ( typeof stringType === 'boolean' ) { if ( ! stringType ) { throw new Error ( 'A non-string type was used to index an object type.' ) ; } const functionNames = propertiesInfo . map ( ( propertyInfo ) => propertyInfo . isMethod ? VisitorUtils . getIgnoredTypeFunction ( visitorContext ) : VisitorTypeCheck . visitType ( propertyInfo . type ! , visitorContext ) ) ; return VisitorUtils . createDisjunctionFunction ( functionNames , name , visitorContext ) ; } else { const strings = sliceSet ( stringType ) ; if ( strings . some ( ( value ) => propertiesInfo . every ( ( propertyInfo ) => propertyInfo . name !== value ) ) ) { throw new Error ( 'Indexed access on object type with an index that does not exist.' ) ; } const stringPropertiesInfo = strings . map ( ( value ) => propertiesInfo . find ( ( propertyInfo ) => propertyInfo . name === value ) ! ) ; const functionNames = stringPropertiesInfo . map ( ( propertyInfo ) => propertyInfo . isMethod ? VisitorUtils . getIgnoredTypeFunction ( visitorContext ) : VisitorTypeCheck . visitType ( propertyInfo . type ! , visitorContext ) ) ; return VisitorUtils . createDisjunctionFunction ( functionNames , name , visitorContext ) ; } } ) ; } function visitTupleObjectType ( type , indexType , visitorContext ) { const name = VisitorTypeName . visitType ( type , visitorContext , { type : 'indexed-access' , indexType } ) ; return VisitorUtils . setFunctionIfNotExists ( name , visitorContext , ( ) => { if ( type . typeArguments === undefined ) { throw new Error ( 'Expected tuple type to have type arguments.' ) ; } const numberType = VisitorIsNumber . visitType ( indexType , visitorContext ) ; if ( typeof numberType === 'boolean' ) { if ( ! numberType ) { throw new Error ( 'A non-number type was used to index a tuple type.' ) ; } const functionNames = type . typeArguments . map ( ( type ) => VisitorTypeCheck . visitType ( type , visitorContext ) ) ; return VisitorUtils . createDisjunctionFunction ( functionNames , name , visitorContext ) ; } else { const numbers = sliceSet ( numberType ) ; if ( numbers . some ( ( value ) => value >= type . typeArguments ! . length ) ) { throw new Error ( 'Indexed access on tuple type exceeds length of tuple.' ) ; } const functionNames = numbers . map ( ( value ) => VisitorTypeCheck . visitType ( type . typeArguments ! [ value ] , visitorContext ) ) ; return VisitorUtils . createDisjunctionFunction ( functionNames , name , visitorContext ) ; } } ) ; } function visitArrayObjectType ( type , indexType , visitorContext ) { const numberIndexType = visitorContext . checker . getIndexTypeOfType ( type , ts . IndexKind . Number ) ; if ( numberIndexType === undefined ) { throw new Error ( 'Expected array ObjectType to have a number index type.' ) ; } const numberType = VisitorIsNumber . visitType ( indexType , visitorContext ) ; if ( numberType !== false ) { return VisitorTypeCheck . visitType ( numberIndexType , visitorContext ) ; } else { throw new Error ( 'A non-number type was used to index an array type.' ) ; } } function visitObjectType ( type , indexType , visitorContext ) { if ( tsutils . isTupleType ( type ) ) { return visitTupleObjectType ( type , indexType , visitorContext ) ; } else if ( visitorContext . checker . getIndexTypeOfType ( type , ts . IndexKind . Number ) ) { return visitArrayObjectType ( type , indexType , visitorContext ) ; } else { return visitRegularObjectType ( type , indexType , visitorContext ) ; } } function visitUnionOrIntersectionType ( type , indexType , visitorContext ) { const name = VisitorTypeName . visitType ( type , visitorContext , { type : 'indexed-access' , indexType } ) ; return VisitorUtils . setFunctionIfNotExists ( name , visitorContext , ( ) => { const functionNames = type . types . map ( ( type ) => visitType ( type , indexType , visitorContext ) ) ; if ( tsutils . isUnionType ( type ) ) { return VisitorUtils . createConjunctionFunction ( functionNames , name ) ; } else { return VisitorUtils . createDisjunctionFunction ( functionNames , name , visitorContext ) ; } } ) ; } function visitIndexType ( ) { throw new Error ( 'Index types cannot be used as indexed types.' ) ; } function visitNonPrimitiveType ( ) { throw new Error ( 'Non-primitive object cannot be used as an indexed type.' ) ; } function visitLiteralType ( ) { throw new Error ( 'Literal strings/numbers cannot be used as an indexed type.' ) ; } function visitTypeReference ( type , indexType , visitorContext ) { const mapping = VisitorUtils . getTypeReferenceMapping ( type , visitorContext ) ; const previousTypeReference = visitorContext . previousTypeReference ; visitorContext . typeMapperStack . push ( mapping ) ; visitorContext . previousTypeReference = type ; const result = visitType ( type . target , indexType , visitorContext ) ; visitorContext . previousTypeReference = previousTypeReference ; visitorContext . typeMapperStack . pop ( ) ; return result ; } function visitTypeParameter ( type , indexType , visitorContext ) { const mappedType = VisitorUtils . getResolvedTypeParameter ( type , visitorContext ) ; if ( mappedType === undefined ) { throw new Error ( 'Unbound type parameter, missing type node.' ) ; } return visitType ( mappedType , indexType , visitorContext ) ; } function visitBigInt ( ) { throw new Error ( 'BigInt cannot be used as an indexed type.' ) ; } function visitBoolean ( ) { throw new Error ( 'Boolean cannot be used as an indexed type.' ) ; } function visitString ( ) { throw new Error ( 'String cannot be used as an indexed type.' ) ; } function visitBooleanLiteral ( ) { throw new Error ( 'True/false cannot be used as an indexed type.' ) ; } function visitNumber ( ) { throw new Error ( 'Number cannot be used as an indexed type.' ) ; } function visitUndefined ( ) { throw new Error ( 'Undefined cannot be used as an indexed type.' ) ; } function visitNull ( ) { throw new Error ( 'Null cannot be used as an indexed type.' ) ; } function visitNever ( visitorContext ) { return VisitorUtils . getNeverFunction ( visitorContext ) ; } function visitUnknown ( visitorContext ) { return VisitorUtils . getUnknownFunction ( visitorContext ) ; } function visitAny ( visitorContext ) { return VisitorUtils . getAnyFunction ( visitorContext ) ; } export function visitType ( type , indexType , visitorContext ) { if ( ( ts . TypeFlags . Any & type . flags ) !== 0 ) { return visitAny ( visitorContext ) ; } else if ( ( ts . TypeFlags . Unknown & type . flags ) !== 0 ) { return visitUnknown ( visitorContext ) ; } else if ( ( ts . TypeFlags . Never & type . flags ) !== 0 ) { return visitNever ( visitorContext ) ; } else if ( ( ts . TypeFlags . Null & type . flags ) !== 0 ) { return visitNull ( ) ; } else if ( ( ts . TypeFlags . Undefined & type . flags ) !== 0 ) { return visitUndefined ( ) ; } else if ( ( ts . TypeFlags . Number & type . flags ) !== 0 ) { return visitNumber ( ) ; } else if ( VisitorUtils . isBigIntType ( type ) ) { return visitBigInt ( ) ; } else if ( ( ts . TypeFlags . Boolean & type . flags ) !== 0 ) { return visitBoolean ( ) ; } else if ( ( ts . TypeFlags . String & type . flags ) !== 0 ) { return visitString ( ) ; } else if ( ( ts . TypeFlags . BooleanLiteral & type . flags ) !== 0 ) { return visitBooleanLiteral ( ) ; } else if ( tsutils . isTypeReference ( type ) && visitorContext . previousTypeReference !== type ) { return visitTypeReference ( type , indexType , visitorContext ) ; } else if ( ( ts . TypeFlags . TypeParameter & type . flags ) !== 0 ) { return visitTypeParameter ( type , indexType , visitorContext ) ; } else if ( tsutils . isObjectType ( type ) ) { return visitObjectType ( type , indexType , visitorContext ) ; } else if ( tsutils . isLiteralType ( type ) ) { return visitLiteralType ( ) ; } else if ( tsutils . isUnionOrIntersectionType ( type ) ) { return visitUnionOrIntersectionType ( type , indexType , visitorContext ) ; } else if ( ( ts . TypeFlags . NonPrimitive & type . flags ) !== 0 ) { return visitNonPrimitiveType ( ) ; } else if ( ( ts . TypeFlags . Index & type . flags ) !== 0 ) { return visitIndexType ( ) ; } else if ( tsutils . isIndexedAccessType ( type ) ) { throw new Error ( 'Not yet implemented.' ) ; } else { throw new Error ( 'Could not generate type-check; unsupported type with flags: ' + type . flags ) ; } }