mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Compile overloaded methods on interfaces and object types (#1333)
This commit is contained in:
@@ -503,6 +503,24 @@ export function requireExactArityValue(
|
||||
? lowerer.mapTypeOf(lowerer.typeOf(contextual))
|
||||
: null;
|
||||
if (mapped && typeEquals(mapped, funcType)) return;
|
||||
// An object-literal METHOD has no contextual type of its own: the
|
||||
// literal's contextual type names the member slot it fills. An
|
||||
// overloaded member's slot holds its erased function, which spells the
|
||||
// completed parameters of an implementation like `m(a?: string)`.
|
||||
if (
|
||||
contextual === null &&
|
||||
ts.isMethodDeclaration(blame) &&
|
||||
blame.parent !== undefined &&
|
||||
ts.isObjectLiteralExpression(blame.parent) &&
|
||||
(ts.isIdentifier(blame.name) || ts.isStringLiteral(blame.name))
|
||||
) {
|
||||
const literalType = lowerer.checker.getContextualType(blame.parent);
|
||||
const member = literalType
|
||||
? lowerer.checker.getPropertyOfType(literalType, blame.name.text)
|
||||
: undefined;
|
||||
const slot = member ? lowerer.memberSlotTypeOf(lowerer.checker.getTypeOfSymbol(member)) : null;
|
||||
if (slot && typeEquals(slot, funcType)) return;
|
||||
}
|
||||
// A union-typed slot (`runner || defaultRunner` under a
|
||||
// `CommandRunner | undefined` context): the value can only inhabit
|
||||
// the union's one func arm — judge by it.
|
||||
|
||||
@@ -61,7 +61,11 @@ import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js";
|
||||
import { isJsSourceFile, locOf, requireSpecOf } from "../program.js";
|
||||
import { lowerJsonRequireCall } from "./json-require.js";
|
||||
import { lowerAssertInstanceCall } from "./assert-instances.js";
|
||||
import { isGenericCallableMemberType, typeKey } from "../type-mapper.js";
|
||||
import {
|
||||
isGenericCallableMemberType,
|
||||
isOverloadedCallableMemberType,
|
||||
typeKey,
|
||||
} from "../type-mapper.js";
|
||||
import { dynFallbackType, jsFuncNameOf, newFnCtx, staticImportNamespaceType } from "./lowerer.js";
|
||||
import {
|
||||
NARROW_FIRST,
|
||||
@@ -8207,6 +8211,30 @@ function lowerObjectAssignHybrid(lowerer: Lowerer, call: ts.CallExpression): IrE
|
||||
return { kind: "recordLit", fields, type: mapped, loc };
|
||||
}
|
||||
|
||||
/** The accessed member is an overload set (isOverloadedCallableMemberType),
|
||||
* also under an optional chain whose access still spells `| undefined`. */
|
||||
function isOverloadedMemberAccess(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
return isOverloadedCallableMemberType(
|
||||
lowerer.checker.getNonNullableType(lowerer.typeOf(access)),
|
||||
lowerer.checker,
|
||||
);
|
||||
}
|
||||
|
||||
/** The result type of the overload a call through an overloaded member
|
||||
* resolved to: the erased slot's result converts to it. A non-generic
|
||||
* overload answers through its declaration, because the resolved signature
|
||||
* of an optional-chain call (`api?.m(x)`) also carries the chain's
|
||||
* undefined; a generic one answers through its instantiation. */
|
||||
function resolvedOverloadResultType(lowerer: Lowerer, call: ts.CallExpression): IrType {
|
||||
const { checker } = lowerer;
|
||||
const signature = checker.getResolvedSignature(call);
|
||||
const decl = signature ? checker.signatureDeclaration(signature) : undefined;
|
||||
const declared = decl ? checker.getSignatureFromDeclaration(decl) : undefined;
|
||||
const source = declared && declared.getTypeParameters().length === 0 ? declared : signature;
|
||||
const result = source ? lowerer.mapTypeOf(checker.getReturnTypeOfSignature(source)) : null;
|
||||
return result ?? lowerer.irTypeOf(call);
|
||||
}
|
||||
|
||||
export function lowerRecordFieldCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -8238,7 +8266,18 @@ export function lowerRecordFieldCall(
|
||||
const callee = lowerer.lowerUnionProperty(access);
|
||||
if (callee?.type.kind !== "func") return null;
|
||||
const args = completeFuncValueArgs(lowerer, call, callee.type, locOf(call));
|
||||
return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(call) };
|
||||
const value: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee,
|
||||
args,
|
||||
type: callee.type.ret,
|
||||
loc: locOf(call),
|
||||
};
|
||||
// An overloaded member's erased result converts to the resolved
|
||||
// overload's type, as on a single record receiver below.
|
||||
return isOverloadedMemberAccess(lowerer, access)
|
||||
? lowerer.coerceInto(call, value, resolvedOverloadResultType(lowerer, call))
|
||||
: value;
|
||||
}
|
||||
const target = lowerer.fieldTarget(access);
|
||||
const loc = locOf(call);
|
||||
@@ -8313,10 +8352,14 @@ export function lowerRecordFieldCall(
|
||||
type: callee.type.ret,
|
||||
loc,
|
||||
};
|
||||
// A generic member's field holds its erased instance: the result
|
||||
// converts to this call's instantiated return type at the call.
|
||||
if (isGenericCallableMemberType(lowerer.typeOf(access), lowerer.checker))
|
||||
// A generic member's field holds its erased instance, and an overloaded
|
||||
// member's field its erased function (the overloads joined): the result
|
||||
// converts to this call's instantiated or resolved return type.
|
||||
const memberType = lowerer.typeOf(access);
|
||||
if (isGenericCallableMemberType(memberType, lowerer.checker))
|
||||
result = lowerer.coerceInto(call, result, lowerer.irTypeOf(call));
|
||||
else if (isOverloadedMemberAccess(lowerer, access))
|
||||
result = lowerer.coerceInto(call, result, resolvedOverloadResultType(lowerer, call));
|
||||
return init ? { kind: "seqExpr", stmts: [init], result, type: result.type, loc } : result;
|
||||
}
|
||||
|
||||
|
||||
@@ -231,9 +231,11 @@ import {
|
||||
containsUnion,
|
||||
describeComponentBlocker,
|
||||
describeRecordMemberBlocker,
|
||||
erasedOverloadMemberType,
|
||||
formatIrType,
|
||||
FETCH_AMBIENT_TYPES,
|
||||
isOsConstantsSymbol,
|
||||
isOverloadedCallableMemberType,
|
||||
isUnitOnlyTsType,
|
||||
isWorkerHandleType,
|
||||
jsOpenObjectType,
|
||||
@@ -7778,6 +7780,15 @@ export class Lowerer {
|
||||
: mapped;
|
||||
}
|
||||
|
||||
/** The runtime type of a record member slot typed `t`: an overloaded
|
||||
* member's erased function (erasedOverloadMemberType — the type a record
|
||||
* shape stores for it), otherwise mapTypeOf. */
|
||||
memberSlotTypeOf(t: ts.Type): IrType | null {
|
||||
if (isOverloadedCallableMemberType(t, this.checker))
|
||||
return erasedOverloadMemberType(t, this.typeCtx) ?? null;
|
||||
return this.mapTypeOf(t);
|
||||
}
|
||||
|
||||
/** The one position where a contextual UNION must not be adopted over the
|
||||
* expression's own: the LEFT operand of `&&`, `||`, or `??`. Adopting a
|
||||
* contextual union is normally safe because tsc proved the value
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
BOOL,
|
||||
bytesOf,
|
||||
canConvertToDyn,
|
||||
canDynCheckTo,
|
||||
CHILD_T,
|
||||
CRYPTOHASH_T,
|
||||
CRYPTOHMAC_T,
|
||||
@@ -3914,158 +3915,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (hybrid) return hybrid;
|
||||
}
|
||||
const callSigs = checker.getCallSignatures(widened);
|
||||
if (callSigs.length === 1) {
|
||||
const sig = callSigs[0]!;
|
||||
if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null;
|
||||
// A SYNTHESIZED rest param (tsc's JS inference for a function body
|
||||
// reading `arguments` — `(...args: any[]) => any` whose args symbol
|
||||
// has no declaration): the dotDotDot check below can't see it, and a
|
||||
// fixed-arity func mapping would LIE about the value's calling
|
||||
// convention. Detected by the param-count mismatch against the
|
||||
// signature's own declaration, excluding an erased `this` parameter.
|
||||
{
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const declParams =
|
||||
sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined;
|
||||
if (
|
||||
declParams !== undefined &&
|
||||
runtimeParameterCount(declParams) !== sig.getParameters().length
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// tsgo never SYNTHESIZES that rest param into the inferred signature
|
||||
// (5.9.3 did — the count mismatch above was the whole detector
|
||||
// there), so the declaration's own body answers directly.
|
||||
if (sigDecl !== undefined && ts.isFunctionLike(sigDecl) && bodyReadsArguments(sigDecl)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const params: IrType[] = [];
|
||||
let typedRest = false;
|
||||
for (const p of sig.getParameters()) {
|
||||
const decl = checker.valueDeclarationOf(p);
|
||||
const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined;
|
||||
|
||||
// arity ABI selected by lambdaSignature. Treating an inferred any[]
|
||||
// declaration as a typed packed-array closure would bypass its boxed
|
||||
// spread/error semantics and change coverage classification.
|
||||
if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null;
|
||||
const optional =
|
||||
decl !== undefined &&
|
||||
ts.isParameter(decl) &&
|
||||
(decl.questionToken !== undefined || decl.initializer !== undefined);
|
||||
let pt = mapType(checker.getTypeOfSymbol(p), ctx);
|
||||
if (
|
||||
decl &&
|
||||
ts.isParameter(decl) &&
|
||||
ts.isSetAccessorDeclaration(decl.parent) &&
|
||||
isJsSourceFile(decl.getSourceFile())
|
||||
)
|
||||
pt = DYN;
|
||||
// An inferred JavaScript binding pattern consumes an iterable; it
|
||||
// does not require the caller's array to have exactly this length.
|
||||
const executor = decl && ts.isParameter(decl) ? decl.parent : null;
|
||||
const construction = executor?.parent;
|
||||
const constructorSymbol =
|
||||
construction && ts.isNewExpression(construction)
|
||||
? checker.getSymbolAtLocation(construction.expression)
|
||||
: undefined;
|
||||
const nativeExecutor =
|
||||
executor &&
|
||||
(ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) &&
|
||||
construction &&
|
||||
ts.isNewExpression(construction) &&
|
||||
construction.arguments?.[0] === executor &&
|
||||
ts.isIdentifier(construction.expression) &&
|
||||
construction.expression.text === "Promise" &&
|
||||
constructorSymbol &&
|
||||
checker
|
||||
.declarationsOf(constructorSymbol)
|
||||
.some((site) => ctx.isStdlibFile(site.getSourceFile()));
|
||||
if (
|
||||
!nativeExecutor &&
|
||||
decl &&
|
||||
ts.isParameter(decl) &&
|
||||
!decl.dotDotDotToken &&
|
||||
!ts.isSetAccessorDeclaration(decl.parent) &&
|
||||
isJsSourceFile(decl.getSourceFile()) &&
|
||||
!decl.type &&
|
||||
!/@(?:param|type)\b/.test(
|
||||
decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()),
|
||||
)
|
||||
)
|
||||
pt = DYN;
|
||||
if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions);
|
||||
// Belt and braces for non-strict type worlds: an optional param's
|
||||
// ABI slot is always the undefined-armed union (strictNullChecks
|
||||
// already spells it that way; arm it here if the world didn't).
|
||||
if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn") {
|
||||
const armed = withUndefinedArm(pt, ctx.unions);
|
||||
if (!armed) return null;
|
||||
pt = armed;
|
||||
}
|
||||
if (!pt) return null;
|
||||
if (rest) {
|
||||
if (pt.kind !== "array") return null;
|
||||
typedRest = true;
|
||||
}
|
||||
// `(value: void) => void` (Promise<void>'s resolve) is callable with
|
||||
// no arguments — a void param is dropped, not a mapping failure.
|
||||
if (pt.kind === "void") continue;
|
||||
params.push(pt);
|
||||
}
|
||||
const retT = checker.getReturnTypeOfSignature(sig);
|
||||
// `() => never` (a throw-only lambda's inferred type) is assignable to
|
||||
// `() => void` and its calls never produce a value — map the return
|
||||
// like declaredReturnType does for declarations. JS null/undefined
|
||||
// returns can read mutable fields, so callable values use the same
|
||||
// checked-native return ABI as their implementations.
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const jsUnitReturn =
|
||||
sigDecl !== undefined &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
(retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0;
|
||||
let ret =
|
||||
jsUnitReturn || (retT.flags & ts.TypeFlags.Any) !== 0
|
||||
? DYN
|
||||
: retT.flags & ts.TypeFlags.Never
|
||||
? VOID
|
||||
: mapType(retT, ctx);
|
||||
if (sigDecl && ts.isGetAccessorDeclaration(sigDecl) && isJsSourceFile(sigDecl.getSourceFile()))
|
||||
ret = DYN;
|
||||
if (sigDecl && isJsSourceFile(sigDecl.getSourceFile()) && ret?.kind === "array") ret = DYN;
|
||||
if (
|
||||
sigDecl &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
!sigDecl.type &&
|
||||
!/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) &&
|
||||
ret?.kind === "record"
|
||||
)
|
||||
ret = DYN;
|
||||
if (
|
||||
sigDecl &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
!sigDecl.type &&
|
||||
!/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) &&
|
||||
(ret?.kind === "date" ||
|
||||
(ret?.kind === "union" &&
|
||||
ctx.unions.get(ret.unionId)?.arms.some((arm) => arm.kind === "func")))
|
||||
)
|
||||
ret = DYN;
|
||||
if (
|
||||
ret &&
|
||||
sigDecl &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
functionCanReturnUndefined(sigDecl)
|
||||
)
|
||||
ret = withUndefinedArm(ret, ctx.unions) ?? ret;
|
||||
if (!ret) return null;
|
||||
return typedRest
|
||||
? { kind: "func", params, ret, rest: true, restAbi: "typed" }
|
||||
: funcOf(params, ret);
|
||||
}
|
||||
if (callSigs.length === 1) return mapCallSignature(callSigs[0]!, ctx);
|
||||
// Records: object types whose members are all data properties (shorthand
|
||||
// methods in type position count — they're func-typed fields) with
|
||||
// mappable types; no call/construct signatures, no index signatures, not
|
||||
@@ -4339,6 +4189,162 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** One non-overloaded call signature's func type (mapTypeInner's
|
||||
* single-signature rule; erasedOverloadMemberType maps each overload of a
|
||||
* member through it). */
|
||||
function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null {
|
||||
const { checker } = ctx;
|
||||
if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null;
|
||||
// A SYNTHESIZED rest param (tsc's JS inference for a function body
|
||||
// reading `arguments` — `(...args: any[]) => any` whose args symbol
|
||||
// has no declaration): the dotDotDot check below can't see it, and a
|
||||
// fixed-arity func mapping would LIE about the value's calling
|
||||
// convention. Detected by the param-count mismatch against the
|
||||
// signature's own declaration, excluding an erased `this` parameter.
|
||||
{
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const declParams =
|
||||
sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined;
|
||||
if (
|
||||
declParams !== undefined &&
|
||||
runtimeParameterCount(declParams) !== sig.getParameters().length
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// tsgo never SYNTHESIZES that rest param into the inferred signature
|
||||
// (5.9.3 did — the count mismatch above was the whole detector
|
||||
// there), so the declaration's own body answers directly.
|
||||
if (sigDecl !== undefined && ts.isFunctionLike(sigDecl) && bodyReadsArguments(sigDecl)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const params: IrType[] = [];
|
||||
let typedRest = false;
|
||||
for (const p of sig.getParameters()) {
|
||||
const decl = checker.valueDeclarationOf(p);
|
||||
const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined;
|
||||
|
||||
// arity ABI selected by lambdaSignature. Treating an inferred any[]
|
||||
// declaration as a typed packed-array closure would bypass its boxed
|
||||
// spread/error semantics and change coverage classification.
|
||||
if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null;
|
||||
const optional =
|
||||
decl !== undefined &&
|
||||
ts.isParameter(decl) &&
|
||||
(decl.questionToken !== undefined || decl.initializer !== undefined);
|
||||
let pt = mapType(checker.getTypeOfSymbol(p), ctx);
|
||||
if (
|
||||
decl &&
|
||||
ts.isParameter(decl) &&
|
||||
ts.isSetAccessorDeclaration(decl.parent) &&
|
||||
isJsSourceFile(decl.getSourceFile())
|
||||
)
|
||||
pt = DYN;
|
||||
// An inferred JavaScript binding pattern consumes an iterable; it
|
||||
// does not require the caller's array to have exactly this length.
|
||||
const executor = decl && ts.isParameter(decl) ? decl.parent : null;
|
||||
const construction = executor?.parent;
|
||||
const constructorSymbol =
|
||||
construction && ts.isNewExpression(construction)
|
||||
? checker.getSymbolAtLocation(construction.expression)
|
||||
: undefined;
|
||||
const nativeExecutor =
|
||||
executor &&
|
||||
(ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) &&
|
||||
construction &&
|
||||
ts.isNewExpression(construction) &&
|
||||
construction.arguments?.[0] === executor &&
|
||||
ts.isIdentifier(construction.expression) &&
|
||||
construction.expression.text === "Promise" &&
|
||||
constructorSymbol &&
|
||||
checker
|
||||
.declarationsOf(constructorSymbol)
|
||||
.some((site) => ctx.isStdlibFile(site.getSourceFile()));
|
||||
if (
|
||||
!nativeExecutor &&
|
||||
decl &&
|
||||
ts.isParameter(decl) &&
|
||||
!decl.dotDotDotToken &&
|
||||
!ts.isSetAccessorDeclaration(decl.parent) &&
|
||||
isJsSourceFile(decl.getSourceFile()) &&
|
||||
!decl.type &&
|
||||
!/@(?:param|type)\b/.test(
|
||||
decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()),
|
||||
)
|
||||
)
|
||||
pt = DYN;
|
||||
if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions);
|
||||
// Belt and braces for non-strict type worlds: an optional param's
|
||||
// ABI slot is always the undefined-armed union (strictNullChecks
|
||||
// already spells it that way; arm it here if the world didn't).
|
||||
if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn") {
|
||||
const armed = withUndefinedArm(pt, ctx.unions);
|
||||
if (!armed) return null;
|
||||
pt = armed;
|
||||
}
|
||||
if (!pt) return null;
|
||||
if (rest) {
|
||||
if (pt.kind !== "array") return null;
|
||||
typedRest = true;
|
||||
}
|
||||
// `(value: void) => void` (Promise<void>'s resolve) is callable with
|
||||
// no arguments — a void param is dropped, not a mapping failure.
|
||||
if (pt.kind === "void") continue;
|
||||
params.push(pt);
|
||||
}
|
||||
const retT = checker.getReturnTypeOfSignature(sig);
|
||||
// `() => never` (a throw-only lambda's inferred type) is assignable to
|
||||
// `() => void` and its calls never produce a value — map the return
|
||||
// like declaredReturnType does for declarations. JS null/undefined
|
||||
// returns can read mutable fields, so callable values use the same
|
||||
// checked-native return ABI as their implementations.
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const jsUnitReturn =
|
||||
sigDecl !== undefined &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
(retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0;
|
||||
let ret =
|
||||
jsUnitReturn || (retT.flags & ts.TypeFlags.Any) !== 0
|
||||
? DYN
|
||||
: retT.flags & ts.TypeFlags.Never
|
||||
? VOID
|
||||
: mapType(retT, ctx);
|
||||
if (sigDecl && ts.isGetAccessorDeclaration(sigDecl) && isJsSourceFile(sigDecl.getSourceFile()))
|
||||
ret = DYN;
|
||||
if (sigDecl && isJsSourceFile(sigDecl.getSourceFile()) && ret?.kind === "array") ret = DYN;
|
||||
if (
|
||||
sigDecl &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
!sigDecl.type &&
|
||||
!/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) &&
|
||||
ret?.kind === "record"
|
||||
)
|
||||
ret = DYN;
|
||||
if (
|
||||
sigDecl &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
!sigDecl.type &&
|
||||
!/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) &&
|
||||
(ret?.kind === "date" ||
|
||||
(ret?.kind === "union" &&
|
||||
ctx.unions.get(ret.unionId)?.arms.some((arm) => arm.kind === "func")))
|
||||
)
|
||||
ret = DYN;
|
||||
if (
|
||||
ret &&
|
||||
sigDecl &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
functionCanReturnUndefined(sigDecl)
|
||||
)
|
||||
ret = withUndefinedArm(ret, ctx.unions) ?? ret;
|
||||
if (!ret) return null;
|
||||
return typedRest
|
||||
? { kind: "func", params, ret, rest: true, restAbi: "typed" }
|
||||
: funcOf(params, ret);
|
||||
}
|
||||
|
||||
/** Retain a literal discriminator before widening erases its values. A
|
||||
* structural match alone is insufficient: `{ kind: "loop", body: ... }`
|
||||
* also fits a smaller `{ kind: "empty" }` record. Choosing that layout
|
||||
@@ -5585,6 +5591,164 @@ export function erasedGenericMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType
|
||||
return erased?.kind === "func" ? erased : undefined;
|
||||
}
|
||||
|
||||
/** An OVERLOADED callable member type (`on(event: "a", h: (x: number) =>
|
||||
* void): () => void; on(event: "b", h: (x: string) => void): () => void`
|
||||
* in a program interface or type literal): two or more call signatures and
|
||||
* nothing else (no data properties, construct signatures, or index
|
||||
* signatures), every one declared in TypeScript program source. Such a
|
||||
* member keeps a record slot holding its erased runtime function
|
||||
* (erasedOverloadMemberType); calls convert per their resolved overload
|
||||
* (lowerRecordFieldCall). */
|
||||
export function isOverloadedCallableMemberType(t: ts.Type, checker: ts.TypeChecker): boolean {
|
||||
const sigs = checker.getCallSignatures(t);
|
||||
if (sigs.length < 2) return false;
|
||||
if (
|
||||
checker.getConstructSignatures(t).length > 0 ||
|
||||
checker.getPropertiesOfType(t).length > 0 ||
|
||||
checker.getIndexInfosOfType(t).length > 0
|
||||
)
|
||||
return false;
|
||||
return sigs.every((sig) => {
|
||||
const decl = checker.signatureDeclaration(sig);
|
||||
if (decl === undefined) return false;
|
||||
if (!ts.isMethodSignatureDeclaration(decl) && !ts.isCallSignatureDeclaration(decl))
|
||||
return false;
|
||||
const file = decl.getSourceFile();
|
||||
return !file.isDeclarationFile && !isJsSourceFile(file);
|
||||
});
|
||||
}
|
||||
|
||||
/** Scalar arms an overload join may combine into one union. */
|
||||
const OVERLOAD_JOIN_SCALARS = new Set(["f64", "string", "bool", "undefinedT", "nullT"]);
|
||||
|
||||
/** The join of one position's types across an overload set: equal types
|
||||
* stay; scalar mixes form their union; anything else is `unknown` when
|
||||
* every joined type crosses the checked-dynamic boundary in both directions
|
||||
* (the call site converts into the slot, the implementation converts out
|
||||
* of it). Differing callbacks (`(x: number) => void` beside `(x: string) =>
|
||||
* void`) therefore travel as checked function values, which any
|
||||
* implementation's own callback type adapts from. Null when no join
|
||||
* exists. */
|
||||
function joinOverloadTypes(types: readonly IrType[], ctx: TypeMapperCtx): IrType | null {
|
||||
const first = types[0]!;
|
||||
if (types.every((t) => typeEquals(t, first))) return first;
|
||||
const getRecord = (id: string) => ctx.shapes.get(id);
|
||||
const getUnion = (id: string) => ctx.unions.get(id);
|
||||
const arms = new Map<string, IrType>();
|
||||
let scalar = true;
|
||||
for (const t of types) {
|
||||
const parts = t.kind === "union" ? (ctx.unions.get(t.unionId)?.arms ?? []) : [t];
|
||||
if (parts.length === 0) scalar = false;
|
||||
for (const part of parts) {
|
||||
if (!OVERLOAD_JOIN_SCALARS.has(part.kind)) scalar = false;
|
||||
arms.set(typeKey(part), part);
|
||||
}
|
||||
}
|
||||
if (scalar && [...arms.values()].some((arm) => !isUnitType(arm))) {
|
||||
const sorted = [...arms.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
return { kind: "union", unionId: ctx.unions.intern(sorted) };
|
||||
}
|
||||
if (
|
||||
types.every(
|
||||
(t) =>
|
||||
t.kind === "dyn" ||
|
||||
(t.kind !== "void" &&
|
||||
canConvertToDyn(t, getRecord, getUnion) &&
|
||||
canDynCheckTo(t, getRecord, getUnion)),
|
||||
)
|
||||
)
|
||||
return DYN;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** An overloaded member's runtime type (isOverloadedCallableMemberType).
|
||||
* JavaScript stores ONE function under the member; overload signatures only
|
||||
* constrain call sites. The slot's type joins the overloads per parameter
|
||||
* position and result (joinOverloadTypes), and a position some overloads
|
||||
* omit is optional. Each overload's own type parameters erase to `unknown`
|
||||
* first. Calls pick their overload statically, convert arguments into the
|
||||
* slot's parameter types and the result back to the overload's; an
|
||||
* implementation converts into the slot like any function value. Undefined
|
||||
* when the set does not join (rest parameters, a void result beside a
|
||||
* value result, a component outside the checked-dynamic boundary): the
|
||||
* member keeps its fence. */
|
||||
export function erasedOverloadMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType | undefined {
|
||||
const funcs: (IrType & { kind: "func" })[] = [];
|
||||
for (const sig of ctx.checker.getCallSignatures(t)) {
|
||||
const own = new Set(sig.getTypeParameters());
|
||||
const outer = ctx.resolveTypeParam;
|
||||
const mapped = mapCallSignature(
|
||||
sig,
|
||||
own.size === 0
|
||||
? ctx
|
||||
: {
|
||||
...ctx,
|
||||
erasingSignature: sig,
|
||||
resolveTypeParam: (param) => (own.has(param) ? DYN : (outer?.(param) ?? null)),
|
||||
},
|
||||
);
|
||||
if (mapped?.kind !== "func" || mapped.rest === true || mapped.argumentsAll === true)
|
||||
return undefined;
|
||||
funcs.push(mapped);
|
||||
}
|
||||
const arity = Math.max(...funcs.map((f) => f.params.length));
|
||||
const params: IrType[] = [];
|
||||
for (let i = 0; i < arity; i++) {
|
||||
const present = funcs.filter((f) => i < f.params.length).map((f) => f.params[i]!);
|
||||
let param = joinOverloadTypes(present, ctx);
|
||||
if (param !== null && present.length < funcs.length && param.kind !== "dyn")
|
||||
param = withUndefinedArm(param, ctx.unions);
|
||||
if (param === null) return undefined;
|
||||
params.push(param);
|
||||
}
|
||||
const voids = funcs.filter((f) => f.ret.kind === "void").length;
|
||||
if (voids !== 0 && voids !== funcs.length) return undefined;
|
||||
const ret =
|
||||
voids === funcs.length
|
||||
? VOID
|
||||
: joinOverloadTypes(
|
||||
funcs.map((f) => f.ret),
|
||||
ctx,
|
||||
);
|
||||
if (ret === null) return undefined;
|
||||
return funcOf(params, ret);
|
||||
}
|
||||
|
||||
/** Why an overloaded member's set has no erased type
|
||||
* (erasedOverloadMemberType): the first overload, and within it the first
|
||||
* parameter or result, that does not map; otherwise the join itself. A
|
||||
* message tail after "its member 'name'". */
|
||||
function describeOverloadSetBlocker(t: ts.Type, ctx: TypeMapperCtx): string {
|
||||
const { checker } = ctx;
|
||||
for (const sig of checker.getCallSignatures(t)) {
|
||||
const own = new Set(sig.getTypeParameters());
|
||||
const outer = ctx.resolveTypeParam;
|
||||
const sigCtx: TypeMapperCtx =
|
||||
own.size === 0
|
||||
? ctx
|
||||
: {
|
||||
...ctx,
|
||||
erasingSignature: sig,
|
||||
resolveTypeParam: (param) => (own.has(param) ? DYN : (outer?.(param) ?? null)),
|
||||
};
|
||||
const mapped = mapCallSignature(sig, sigCtx);
|
||||
if (mapped?.kind === "func" && mapped.rest !== true && mapped.argumentsAll !== true) continue;
|
||||
const decl = checker.signatureDeclaration(sig);
|
||||
const spelled = decl ? decl.getText().replace(/\s+/g, " ").replace(/;$/, "") : "";
|
||||
const which = spelled ? `overload '${spelled}'` : "one overload";
|
||||
if (mapped?.kind === "func")
|
||||
return `is an overload set, and its ${which} has a rest parameter (an overload set stores one fixed-arity function)`;
|
||||
for (const param of sig.getParameters()) {
|
||||
const paramTs = checker.getTypeOfSymbol(param);
|
||||
if (!mapType(paramTs, sigCtx))
|
||||
return `is an overload set, and its ${which} has parameter '${param.name}' of type '${checker.typeToString(paramTs)}', which does not compile`;
|
||||
}
|
||||
const retTs = checker.getReturnTypeOfSignature(sig);
|
||||
return `is an overload set, and its ${which} returns '${checker.typeToString(retTs)}', which does not compile`;
|
||||
}
|
||||
return "is an overload set whose signatures do not join into one stored function (a void result beside a value result, or positions whose differing types do not cross the checked-dynamic boundary)";
|
||||
}
|
||||
|
||||
/** The computed pieces of a record shape mapRecordTypeInner hands back for
|
||||
* the OUTER frame to intern — or, when a back-reference minted a recursive
|
||||
* placeholder for the type mid-construction, to finalize INTO that
|
||||
@@ -5966,9 +6130,14 @@ function mapRecordTypeInner(
|
||||
// can hold them — see isGenericCallableMemberType): the shape keeps
|
||||
// its data fields, and calls of the member monomorphize per call
|
||||
// site against the defining object literal's declaration.
|
||||
// OVERLOADED members hold their erased runtime function (see
|
||||
// erasedOverloadMemberType); a set that does not join fails the
|
||||
// shape as before.
|
||||
let pt = isGenericCallableMemberType(fieldTs, checker)
|
||||
? erasedGenericMemberType(fieldTs, ctx)
|
||||
: mapType(fieldTs, ctx);
|
||||
: isOverloadedCallableMemberType(fieldTs, checker)
|
||||
? (erasedOverloadMemberType(fieldTs, ctx) ?? null)
|
||||
: mapType(fieldTs, ctx);
|
||||
if (pt === undefined) continue;
|
||||
// tsgo PANICS computing `readonly []` through the symbol-type query
|
||||
// (the TupleType conversion — the facade's panic fence answers
|
||||
@@ -6289,6 +6458,10 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx
|
||||
return null;
|
||||
}
|
||||
const fieldTs = checker.getTypeOfSymbol(p);
|
||||
if (isOverloadedCallableMemberType(fieldTs, checker)) {
|
||||
if (erasedOverloadMemberType(fieldTs, ctx) !== undefined) continue;
|
||||
return `the record shape is supported, but its member '${p.name}' ${describeOverloadSetBlocker(fieldTs, ctx)}`;
|
||||
}
|
||||
let pt = isGenericCallableMemberType(fieldTs, checker)
|
||||
? erasedGenericMemberType(fieldTs, ctx)
|
||||
: mapType(fieldTs, ctx);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6180-interface-overload-events.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6181-interface-overload-arity.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6182-interface-overload-types.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6183-interface-overload-flow.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/overloaded-members.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// An interface method with literal-discriminated overloads (the event
|
||||
// registry shape: on("x", (e: X) => R)) holds ONE function at runtime, as
|
||||
// JavaScript does. Each call site resolves its overload statically; handlers
|
||||
// whose types differ per overload cross into the stored function as checked
|
||||
// function values, and the implementation receives them in its own
|
||||
// callback type. Events keep their identity across that boundary.
|
||||
export {};
|
||||
|
||||
interface SaveEvent {
|
||||
type: "save";
|
||||
path: string;
|
||||
cancel: boolean;
|
||||
}
|
||||
interface LoadEvent {
|
||||
type: "load";
|
||||
path: string;
|
||||
}
|
||||
interface Context {
|
||||
cwd: string;
|
||||
}
|
||||
interface Api {
|
||||
on(event: "save", handler: (e: SaveEvent, ctx: Context) => void): () => void;
|
||||
on(event: "load", handler: (e: LoadEvent, ctx: Context) => boolean): () => void;
|
||||
on(event: "tick", handler: (n: number) => void): () => void;
|
||||
name: string;
|
||||
}
|
||||
|
||||
let nextId = 0;
|
||||
const saves: { id: number; h: (e: SaveEvent, ctx: Context) => void }[] = [];
|
||||
const loads: { id: number; h: (e: LoadEvent, ctx: Context) => boolean }[] = [];
|
||||
const ticks: { id: number; h: (n: number) => void }[] = [];
|
||||
|
||||
function createApi(name: string): Api {
|
||||
return {
|
||||
name,
|
||||
on(event: "save" | "load" | "tick", handler: any): () => void {
|
||||
const id = ++nextId;
|
||||
const count =
|
||||
event === "save"
|
||||
? saves.push({ id, h: handler })
|
||||
: event === "load"
|
||||
? loads.push({ id, h: handler })
|
||||
: ticks.push({ id, h: handler });
|
||||
console.log(`${name}: registered ${event} #${id} (${count})`);
|
||||
return () => {
|
||||
if (event === "save") saves.splice(saves.findIndex((x) => x.id === id), 1);
|
||||
else if (event === "load") loads.splice(loads.findIndex((x) => x.id === id), 1);
|
||||
else ticks.splice(ticks.findIndex((x) => x.id === id), 1);
|
||||
console.log(`${name}: removed ${event} #${id}`);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// An extension sees only the interface.
|
||||
function extension(pi: Api): () => void {
|
||||
pi.on("save", (e, ctx) => {
|
||||
e.cancel = e.path.endsWith(".lock");
|
||||
console.log(`save ${ctx.cwd}/${e.path}`);
|
||||
});
|
||||
pi.on("load", (e) => e.path.length > 3);
|
||||
return pi.on("tick", (n) => console.log("tick", n * 2, pi.name));
|
||||
}
|
||||
|
||||
const api = createApi("main");
|
||||
const stopTicks = extension(api);
|
||||
const ctx: Context = { cwd: "/work" };
|
||||
for (const path of ["a.txt", "b.lock"]) {
|
||||
const ev: SaveEvent = { type: "save", path, cancel: false };
|
||||
for (const { h } of saves) h(ev, ctx);
|
||||
console.log(path, "cancelled:", ev.cancel);
|
||||
}
|
||||
for (const { h } of loads) console.log(h({ type: "load", path: "ab" }, ctx), h({ type: "load", path: "abcd" }, ctx));
|
||||
for (const { h } of ticks) h(21);
|
||||
stopTicks();
|
||||
console.log("ticks left", ticks.length);
|
||||
|
||||
// A named handler value converts the same way as a lambda.
|
||||
function onTick(n: number): void {
|
||||
console.log("named tick", n);
|
||||
}
|
||||
const off = api.on("tick", onTick);
|
||||
for (const { h } of ticks) h(7);
|
||||
off();
|
||||
|
||||
// A handler that throws propagates through the boundary unchanged.
|
||||
api.on("load", (e) => {
|
||||
if (e.path === "") throw new Error("empty path");
|
||||
return true;
|
||||
});
|
||||
try {
|
||||
for (const { h } of loads) h({ type: "load", path: "" }, ctx);
|
||||
} catch (err) {
|
||||
console.log("caught", (err as Error).message);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Arity overloads on interface and type-literal members: positions some
|
||||
// overloads omit are optional in the stored function, so one implementation
|
||||
// with optional parameters serves every overload, and a call omitting a
|
||||
// trailing argument passes undefined exactly as Node does.
|
||||
export {};
|
||||
|
||||
interface Formatter {
|
||||
label(): string;
|
||||
label(prefix: string): string;
|
||||
label(prefix: string, width: number): string;
|
||||
}
|
||||
const formatter: Formatter = {
|
||||
label(prefix?: string, width?: number): string {
|
||||
const text = prefix ?? "none";
|
||||
return width === undefined ? text : text.padEnd(width, ".") + "|";
|
||||
},
|
||||
};
|
||||
console.log(formatter.label());
|
||||
console.log(formatter.label("p"));
|
||||
console.log(formatter.label("q", 6));
|
||||
|
||||
// Overloads spelled as call signatures of a type-literal member.
|
||||
type Logger = {
|
||||
log: {
|
||||
(message: string): void;
|
||||
(level: number, message: string): void;
|
||||
};
|
||||
lines: string[];
|
||||
};
|
||||
const lines: string[] = [];
|
||||
const logger: Logger = {
|
||||
lines,
|
||||
log: (a: string | number, b?: string) => {
|
||||
lines.push(typeof a === "number" ? `[${a}] ${b}` : a);
|
||||
},
|
||||
};
|
||||
logger.log("plain");
|
||||
logger.log(2, "leveled");
|
||||
console.log(logger.lines.join("; "));
|
||||
|
||||
// A single-signature method with an optional parameter fills its slot the
|
||||
// same way.
|
||||
interface Greeter {
|
||||
greet(name?: string): string;
|
||||
}
|
||||
const greeter: Greeter = {
|
||||
greet(name?: string): string {
|
||||
return `hello ${name ?? "stranger"}`;
|
||||
},
|
||||
};
|
||||
console.log(greeter.greet(), greeter.greet("ada"));
|
||||
|
||||
// Arguments are evaluated once, left to right, before the call.
|
||||
let n = 0;
|
||||
const next = (): string => `#${++n}`;
|
||||
console.log(formatter.label(next(), 4 + n), n);
|
||||
@@ -0,0 +1,61 @@
|
||||
// Type overloads with one shared implementation: the stored function takes
|
||||
// the join of the overload parameter types (a scalar union here) and each
|
||||
// call converts its result back to the resolved overload's type. Records,
|
||||
// generic overloads, and implementations typed with unions or `any` share
|
||||
// the same slot.
|
||||
export {};
|
||||
|
||||
interface Converter {
|
||||
conv(x: number): string;
|
||||
conv(x: string): number;
|
||||
conv(x: boolean): boolean;
|
||||
}
|
||||
const converter: Converter = {
|
||||
conv(x: number | string | boolean): any {
|
||||
return typeof x === "number" ? `n${x}` : typeof x === "string" ? x.length : !x;
|
||||
},
|
||||
};
|
||||
const s: string = converter.conv(12);
|
||||
const len: number = converter.conv("abcd");
|
||||
const flipped: boolean = converter.conv(false);
|
||||
console.log(s.toUpperCase(), len + 1, flipped);
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
interface Named {
|
||||
name: string;
|
||||
}
|
||||
interface Factory {
|
||||
make(kind: "point", a: number, b: number): Point;
|
||||
make(kind: "named", a: string): Named;
|
||||
}
|
||||
const factory: Factory = {
|
||||
make(kind: string, a: number | string, b?: number): any {
|
||||
return kind === "point" ? { x: a, y: b } : { name: a };
|
||||
},
|
||||
};
|
||||
const p = factory.make("point", 3, 4);
|
||||
const named = factory.make("named", "zed");
|
||||
console.log(Math.hypot(p.x, p.y), named.name.length, p, named);
|
||||
|
||||
// Generic overloads erase their own type parameters to `unknown`.
|
||||
interface Store {
|
||||
get(key: "names", fallback: string[]): string[];
|
||||
get(key: "count"): number;
|
||||
at<T>(items: T[], i: number): T;
|
||||
at(items: string, i: number): string;
|
||||
}
|
||||
const data: Record<string, unknown> = { names: ["a", "b"], count: 3 };
|
||||
const store: Store = {
|
||||
get(key: string, fallback?: unknown): any {
|
||||
return data[key] ?? fallback;
|
||||
},
|
||||
at(items: any, i: number): any {
|
||||
return items[i];
|
||||
},
|
||||
};
|
||||
console.log(store.get("names", []).join("|"), store.get("count") * 2);
|
||||
console.log(store.at([10, 20, 30], 1) + 1, store.at("xyz", 2).toUpperCase());
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Values of an interface with an overloaded member flow like any record:
|
||||
// through arrays, factory callbacks, and function results. A class that
|
||||
// implements the interface dispatches to its own overloaded method.
|
||||
export {};
|
||||
|
||||
interface Bus {
|
||||
emit(kind: "num", value: number): void;
|
||||
emit(kind: "text", value: string): void;
|
||||
label: string;
|
||||
}
|
||||
type Plugin = (bus: Bus) => void;
|
||||
|
||||
const log: string[] = [];
|
||||
function makeBus(label: string): Bus {
|
||||
return {
|
||||
label,
|
||||
emit(kind: "num" | "text", value: number | string): void {
|
||||
log.push(`${label}:${kind}:${typeof value}:${value}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
const plugins: Plugin[] = [
|
||||
(bus) => bus.emit("num", 1),
|
||||
(bus) => {
|
||||
bus.emit("text", "two");
|
||||
bus.emit("num", bus.label.length + 2);
|
||||
},
|
||||
];
|
||||
const buses: Bus[] = [makeBus("x"), makeBus("yy")];
|
||||
for (const bus of buses) for (const plugin of plugins) plugin(bus);
|
||||
console.log(log.join("\n"));
|
||||
console.log(typeof buses[1]!.emit, JSON.stringify(buses[0]));
|
||||
|
||||
interface Picker {
|
||||
pick(kind: "s"): string;
|
||||
pick(kind: "n"): number;
|
||||
}
|
||||
class Impl implements Picker {
|
||||
private readonly base: number;
|
||||
constructor(base: number) {
|
||||
this.base = base;
|
||||
}
|
||||
pick(kind: "s"): string;
|
||||
pick(kind: "n"): number;
|
||||
pick(kind: "s" | "n"): string | number {
|
||||
return kind === "s" ? `str${this.base}` : this.base * 7;
|
||||
}
|
||||
}
|
||||
function use(p: Picker): string {
|
||||
return `${p.pick("s").length}/${p.pick("n") + 1}`;
|
||||
}
|
||||
console.log(use(new Impl(1)), use(new Impl(10)));
|
||||
const literal: Picker = {
|
||||
pick(kind: "s" | "n"): any {
|
||||
return kind === "s" ? "literal" : -1;
|
||||
},
|
||||
};
|
||||
console.log(use(literal));
|
||||
|
||||
// Receivers of every shape resolve the overload the same way: a union of
|
||||
// records sharing the member, an optional chain, and a call result.
|
||||
interface Measure {
|
||||
kind: "len";
|
||||
of(x: number): string;
|
||||
of(x: string): number;
|
||||
}
|
||||
interface Sign {
|
||||
kind: "sign";
|
||||
of(x: number): string;
|
||||
of(x: string): number;
|
||||
}
|
||||
const measure: Measure = {
|
||||
kind: "len",
|
||||
of(x: number | string): any {
|
||||
return typeof x === "number" ? `#${x}` : x.length;
|
||||
},
|
||||
};
|
||||
const sign: Sign = {
|
||||
kind: "sign",
|
||||
of(x: number | string): any {
|
||||
return typeof x === "number" ? (x < 0 ? "-" : "+") : -x.length;
|
||||
},
|
||||
};
|
||||
for (const m of [measure, sign] as (Measure | Sign)[]) console.log(m.kind, m.of(-4).length, m.of("xyz") + 1);
|
||||
const maybe: Measure | undefined = buses.length > 5 ? undefined : measure;
|
||||
const none: Measure | undefined = buses.length > 5 ? measure : undefined;
|
||||
console.log(maybe?.of(5), maybe?.of("abc"), none?.of(1), none?.of("q"));
|
||||
const pickMeasure = (): Measure => measure;
|
||||
console.log(pickMeasure().of(7).toUpperCase());
|
||||
@@ -0,0 +1,25 @@
|
||||
// Overloaded interface members compile as one stored function whose type
|
||||
// joins the overloads; calls resolve per site. Two fences remain: reading
|
||||
// the member as a VALUE (its type is still the overload set), and a set
|
||||
// whose overloads do not join (here, a rest parameter beside a fixed one).
|
||||
interface Api {
|
||||
on(event: "a", handler: (x: number) => void): void;
|
||||
on(event: "b", handler: (x: string) => void): void;
|
||||
}
|
||||
const api: Api = {
|
||||
on(event: string, handler: (x: any) => void): void {
|
||||
handler(event === "a" ? 1 : "one");
|
||||
},
|
||||
};
|
||||
api.on("a", (x) => console.log(x + 1));
|
||||
const detached = api.on;
|
||||
console.log(typeof detached);
|
||||
|
||||
interface Emitter {
|
||||
emit(event: "many", ...values: number[]): void;
|
||||
emit(event: "one", value: string): void;
|
||||
}
|
||||
function fire(e: Emitter): void {
|
||||
e.emit("many", 1, 2);
|
||||
}
|
||||
console.log(typeof fire);
|
||||
@@ -0,0 +1,35 @@
|
||||
overloaded-members.ts:15:7 - error SC2007: values of type '{ (event: "a", handler: (x: number) => void): void; (event: "b", handler: (x: string) => void): void; }' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature
|
||||
|
||||
14 | api.on("a", (x) => console.log(x + 1));
|
||||
15 | const detached = api.on;
|
||||
| ^~~~~~~~
|
||||
16 | console.log(typeof detached);
|
||||
|
||||
hint: annotate the slot with the ONE signature the program actually calls (e.g. '(x: number) => string'), or wrap the overloaded function in a single-signature arrow
|
||||
|
||||
overloaded-members.ts:16:20 - error SC2004: uses of 'detached' inherit the blocker on its declaration
|
||||
|
||||
15 | const detached = api.on;
|
||||
16 | console.log(typeof detached);
|
||||
| ^~~~~~~~
|
||||
17 |
|
||||
|
||||
hint: the declaration of 'detached' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
|
||||
overloaded-members.ts:22:15 - error SC2009: values of type 'Emitter' cannot be compiled: the record shape is supported, but its member 'emit' is an overload set, and its overload 'emit(event: "many", ...values: number[]): void' has a rest parameter (an overload set stores one fixed-arity function)
|
||||
|
||||
21 | }
|
||||
22 | function fire(e: Emitter): void {
|
||||
| ^
|
||||
23 | e.emit("many", 1, 2);
|
||||
|
||||
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
|
||||
|
||||
overloaded-members.ts:25:20 - error SC2004: uses of 'fire' inherit the blocker on its declaration
|
||||
|
||||
24 | }
|
||||
25 | console.log(typeof fire);
|
||||
| ^~~~
|
||||
26 |
|
||||
|
||||
hint: the declaration of 'fire' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
Reference in New Issue
Block a user